<commit_before>package controller

import (
	"context"
	"fmt"
	"strconv"

	"github.com/golang/glog"
	corev1 "k8s.io/api/core/v1"
	extv1beta1 "k8s.io/api/extensions/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)

const (
	// tlsACMEAnnotation is here for compatibility with kube-lego style
	// ingress resources. When set to "true", a Certificate resource with
	// the default configuration provided to ingress-annotation should be
	// created.
	tlsACMEAnnotation = "kubernetes.io/tls-acme"
	// issuerNameAnnotation can be used to override the issuer specified on the
	// created Certificate resource.
	issuerNameAnnotation = "certmanager.k8s.io/issuer"
	// clusterIssuerNameAnnotation can be used to override the issuer specified on the
	// created Certificate resource. The Certificate will reference the
	// specified *ClusterIssuer* instead of normal issuer.
	clusterIssuerNameAnnotation = "certmanager.k8s.io/cluster-issuer"
	// acmeIssuerChallengeTypeAnnotation can be used to override the default ACME challenge
	// type to be used when the specified issuer is an ACME issuer
	acmeIssuerChallengeTypeAnnotation = "certmanager.k8s.io/acme-challenge-type"
	// acmeIssuerDNS01ProviderNameAnnotation can be used to override the default dns01 provider
	// configured on the issuer if the challenge type is set to dns01
	acmeIssuerDNS01ProviderNameAnnotation = "certmanager.k8s.io/acme-dns01-provider"
)

var ingressGVK = extv1beta1.SchemeGroupVersion.WithKind("Ingress")

func (c *Controller) Sync(ctx context.Context, ing *extv1beta1.Ingress) error {
	if !shouldSync(ing) {
		glog.Infof("Not syncing ingress %s/%s as it does not contain necessary annotations", ing.Namespace, ing.Name)
		return nil
	}

	newCrts, updateCrts, err := c.buildCertificates(ing)
	if err != nil {
		return err
	}

	for _, crt := range newCrts {
		_, err := c.CMClient.CertmanagerV1alpha1().Certificates(crt.Namespace).Create(crt)
		if err != nil {
			return err
		}
		c.Recorder.Eventf(ing, corev1.EventTypeNormal, "CreateCertificate", "Successfully created Certificate %q", crt.Name)
	}

	for _, crt := range updateCrts {
		_, err := c.CMClient.CertmanagerV1alpha1().Certificates(crt.Namespace).Update(crt)
		if err != nil {
			return err
		}
		c.Recorder.Eventf(ing, corev1.EventTypeNormal, "UpdateCertificate", "Successfully updated Certificate %q", crt.Name)
	}

	return nil
}

func (c *Controller) buildCertificates(ing *extv1beta1.Ingress) (new, update []*v1alpha1.Certificate, _ error) {
	issuerName, issuerKind := c.issuerForIngress(ing)
	issuer, err := c.getGenericIssuer(ing.Namespace, issuerName, issuerKind)
	if err != nil {
		return nil, nil, err
	}

	var newCrts []*v1alpha1.Certificate
	var updateCrts []*v1alpha1.Certificate
	for i, tls := range ing.Spec.TLS {
		// validate the ingress TLS block
		if len(tls.Hosts) == 0 {
			return nil, nil, fmt.Errorf("secret %q for ingress %q has no hosts specified", tls.SecretName, ing.Name)
		}
		if tls.SecretName == "" {
			return nil, nil, fmt.Errorf("TLS entry %d for ingress %q must specify a secretName", i, ing.Name)
		}

		existingCrt, err := c.certificateLister.Certificates(ing.Namespace).Get(tls.SecretName)
		if !apierrors.IsNotFound(err) && err != nil {
			return nil, nil, err
		}

		crt := &v1alpha1.Certificate{
			ObjectMeta: metav1.ObjectMeta{
				Name:            tls.SecretName,
				Namespace:       ing.Namespace,
				OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ing, ingressGVK)},
			},
			Spec: v1alpha1.CertificateSpec{
				DNSNames:   tls.Hosts,
				SecretName: tls.SecretName,
				IssuerRef: v1alpha1.ObjectReference{
					Name: issuerName,
					Kind: issuerKind,
				},
			},
		}

		err = c.setIssuerSpecificConfig(crt, issuer, ing, tls)
		if err != nil {
			return nil, nil, err
		}

		// check if a Certificate for this TLS entry already exists, and if it
		// does then skip this entry
		if existingCrt != nil {
			glog.Infof("Certificate %q for ingress %q already exists", tls.SecretName, ing.Name)

			if crtEqual(existingCrt, crt) {
				glog.Infof("Certificate %q for ingress %q is up to date", tls.SecretName, ing.Name)
				continue
			}

			updateCrt := existingCrt.DeepCopy()

			updateCrt.Spec.DNSNames = tls.Hosts
			updateCrt.Spec.SecretName = tls.SecretName
			updateCrt.Spec.IssuerRef.Name = issuerName
			updateCrt.Spec.IssuerRef.Kind = issuerKind
			updateCrts = append(updateCrts, updateCrt)
		} else {
			newCrts = append(newCrts, crt)
		}
	}
	return newCrts, updateCrts, nil
}

// crtEqual checks and returns true if two Certificates are equal
func crtEqual(a, b *v1alpha1.Certificate) bool {
	if a.Name != b.Name {
		return false
	}

	if len(a.Spec.DNSNames) != len(b.Spec.DNSNames) {
		return false
	}

	for i := range a.Spec.DNSNames {
		if a.Spec.DNSNames[i] != b.Spec.DNSNames[i] {
			return false
		}
	}

	if a.Spec.SecretName != b.Spec.SecretName {
		return false
	}

	if a.Spec.IssuerRef.Name != b.Spec.IssuerRef.Name {
		return false
	}

	if a.Spec.IssuerRef.Kind != b.Spec.IssuerRef.Kind {
		return false
	}

	return true
}

func (c *Controller) setIssuerSpecificConfig(crt *v1alpha1.Certificate, issuer v1alpha1.GenericIssuer, ing *extv1beta1.Ingress, tls extv1beta1.IngressTLS) error {
	ingAnnotations := ing.Annotations
	if ingAnnotations == nil {
		ingAnnotations = map[string]string{}
	}
	// for ACME issuers
	if issuer.GetSpec().ACME != nil {
		challengeType, ok := ingAnnotations[acmeIssuerChallengeTypeAnnotation]
		if !ok {
			challengeType = c.options.DefaultACMEIssuerChallengeType
		}
		domainCfg := v1alpha1.ACMECertificateDomainConfig{
			Domains: tls.Hosts,
		}
		switch challengeType {
		case "http01":
			domainCfg.HTTP01 = &v1alpha1.ACMECertificateHTTP01Config{Ingress: ing.Name}
		case "dns01":
			dnsProvider, ok := ingAnnotations[acmeIssuerDNS01ProviderNameAnnotation]
			if !ok {
				dnsProvider = c.options.DefaultACMEIssuerDNS01ProviderName
			}
			if dnsProvider == "" {
				return fmt.Errorf("no acme issuer dns01 challenge provider specified")
			}
			domainCfg.DNS01 = &v1alpha1.ACMECertificateDNS01Config{Provider: dnsProvider}
		default:
			return fmt.Errorf("invalid acme issuer challenge type specified %q", challengeType)
		}
		crt.Spec.ACME = &v1alpha1.ACMECertificateConfig{Config: []v1alpha1.ACMECertificateDomainConfig{domainCfg}}
	}
	return nil
}

// shouldSync returns true if this ingress should have a Certificate resource
// created for it
func shouldSync(ing *extv1beta1.Ingress) bool {
	annotations := ing.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	if _, ok := annotations[issuerNameAnnotation]; ok {
		return true
	}
	if _, ok := annotations[clusterIssuerNameAnnotation]; ok {
		return true
	}
	if s, ok := annotations[tlsACMEAnnotation]; ok {
		if b, _ := strconv.ParseBool(s); b {
			return true
		}
	}
	if _, ok := annotations[acmeIssuerChallengeTypeAnnotation]; ok {
		return true
	}
	if _, ok := annotations[acmeIssuerDNS01ProviderNameAnnotation]; ok {
		return true
	}
	return false
}

// issuerForIngress will determine the issuer that should be specified on a
// Certificate created for the given Ingress resource. If one is not set, the
// default issuer given to the controller will be used.
func (c *Controller) issuerForIngress(ing *extv1beta1.Ingress) (name string, kind string) {
	name = c.options.DefaultIssuerName
	kind = c.options.DefaultIssuerKind
	annotations := ing.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	if issuerName, ok := annotations[issuerNameAnnotation]; ok {
		name = issuerName
		kind = v1alpha1.IssuerKind
	}
	if issuerName, ok := annotations[clusterIssuerNameAnnotation]; ok {
		name = issuerName
		kind = v1alpha1.ClusterIssuerKind
	}
	return name, kind
}

func (c *Controller) getGenericIssuer(namespace, name, kind string) (v1alpha1.GenericIssuer, error) {
	switch kind {
	case v1alpha1.IssuerKind:
		return c.issuerLister.Issuers(namespace).Get(name)
	case v1alpha1.ClusterIssuerKind:
		if c.clusterIssuerLister == nil {
			return nil, fmt.Errorf("cannot get ClusterIssuer for %q as ingress-shim is scoped to a single namespace", name)
		}
		return c.clusterIssuerLister.Get(name)
	default:
		return nil, fmt.Errorf(`invalid value %q for issuer kind. Must be empty, %q or %q`, kind, v1alpha1.IssuerKind, v1alpha1.ClusterIssuerKind)
	}
}
<commit_msg>crtEqual -> certNeedsUpdate<commit_after>package controller

import (
	"context"
	"fmt"
	"strconv"

	"github.com/golang/glog"
	corev1 "k8s.io/api/core/v1"
	extv1beta1 "k8s.io/api/extensions/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)

const (
	// tlsACMEAnnotation is here for compatibility with kube-lego style
	// ingress resources. When set to "true", a Certificate resource with
	// the default configuration provided to ingress-annotation should be
	// created.
	tlsACMEAnnotation = "kubernetes.io/tls-acme"
	// issuerNameAnnotation can be used to override the issuer specified on the
	// created Certificate resource.
	issuerNameAnnotation = "certmanager.k8s.io/issuer"
	// clusterIssuerNameAnnotation can be used to override the issuer specified on the
	// created Certificate resource. The Certificate will reference the
	// specified *ClusterIssuer* instead of normal issuer.
	clusterIssuerNameAnnotation = "certmanager.k8s.io/cluster-issuer"
	// acmeIssuerChallengeTypeAnnotation can be used to override the default ACME challenge
	// type to be used when the specified issuer is an ACME issuer
	acmeIssuerChallengeTypeAnnotation = "certmanager.k8s.io/acme-challenge-type"
	// acmeIssuerDNS01ProviderNameAnnotation can be used to override the default dns01 provider
	// configured on the issuer if the challenge type is set to dns01
	acmeIssuerDNS01ProviderNameAnnotation = "certmanager.k8s.io/acme-dns01-provider"
)

var ingressGVK = extv1beta1.SchemeGroupVersion.WithKind("Ingress")

func (c *Controller) Sync(ctx context.Context, ing *extv1beta1.Ingress) error {
	if !shouldSync(ing) {
		glog.Infof("Not syncing ingress %s/%s as it does not contain necessary annotations", ing.Namespace, ing.Name)
		return nil
	}

	newCrts, updateCrts, err := c.buildCertificates(ing)
	if err != nil {
		return err
	}

	for _, crt := range newCrts {
		_, err := c.CMClient.CertmanagerV1alpha1().Certificates(crt.Namespace).Create(crt)
		if err != nil {
			return err
		}
		c.Recorder.Eventf(ing, corev1.EventTypeNormal, "CreateCertificate", "Successfully created Certificate %q", crt.Name)
	}

	for _, crt := range updateCrts {
		_, err := c.CMClient.CertmanagerV1alpha1().Certificates(crt.Namespace).Update(crt)
		if err != nil {
			return err
		}
		c.Recorder.Eventf(ing, corev1.EventTypeNormal, "UpdateCertificate", "Successfully updated Certificate %q", crt.Name)
	}

	return nil
}

func (c *Controller) buildCertificates(ing *extv1beta1.Ingress) (new, update []*v1alpha1.Certificate, _ error) {
	issuerName, issuerKind := c.issuerForIngress(ing)
	issuer, err := c.getGenericIssuer(ing.Namespace, issuerName, issuerKind)
	if err != nil {
		return nil, nil, err
	}

	var newCrts []*v1alpha1.Certificate
	var updateCrts []*v1alpha1.Certificate
	for i, tls := range ing.Spec.TLS {
		// validate the ingress TLS block
		if len(tls.Hosts) == 0 {
			return nil, nil, fmt.Errorf("secret %q for ingress %q has no hosts specified", tls.SecretName, ing.Name)
		}
		if tls.SecretName == "" {
			return nil, nil, fmt.Errorf("TLS entry %d for ingress %q must specify a secretName", i, ing.Name)
		}

		existingCrt, err := c.certificateLister.Certificates(ing.Namespace).Get(tls.SecretName)
		if !apierrors.IsNotFound(err) && err != nil {
			return nil, nil, err
		}

		crt := &v1alpha1.Certificate{
			ObjectMeta: metav1.ObjectMeta{
				Name:            tls.SecretName,
				Namespace:       ing.Namespace,
				OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(ing, ingressGVK)},
			},
			Spec: v1alpha1.CertificateSpec{
				DNSNames:   tls.Hosts,
				SecretName: tls.SecretName,
				IssuerRef: v1alpha1.ObjectReference{
					Name: issuerName,
					Kind: issuerKind,
				},
			},
		}

		err = c.setIssuerSpecificConfig(crt, issuer, ing, tls)
		if err != nil {
			return nil, nil, err
		}

		// check if a Certificate for this TLS entry already exists, and if it
		// does then skip this entry
		if existingCrt != nil {
			glog.Infof("Certificate %q for ingress %q already exists", tls.SecretName, ing.Name)

			if !certNeedsUpdate(existingCrt, crt) {
				glog.Infof("Certificate %q for ingress %q is up to date", tls.SecretName, ing.Name)
				continue
			}

			updateCrt := existingCrt.DeepCopy()

			updateCrt.Spec.DNSNames = tls.Hosts
			updateCrt.Spec.SecretName = tls.SecretName
			updateCrt.Spec.IssuerRef.Name = issuerName
			updateCrt.Spec.IssuerRef.Kind = issuerKind
			updateCrts = append(updateCrts, updateCrt)
		} else {
			newCrts = append(newCrts, crt)
		}
	}
	return newCrts, updateCrts, nil
}

// certNeedsUpdate checks and returns true if two Certificates are equal
func certNeedsUpdate(a, b *v1alpha1.Certificate) bool {
	if a.Name != b.Name {
		return true
	}

	if len(a.Spec.DNSNames) != len(b.Spec.DNSNames) {
		return true
	}

	for i := range a.Spec.DNSNames {
		if a.Spec.DNSNames[i] != b.Spec.DNSNames[i] {
			return true
		}
	}

	if a.Spec.SecretName != b.Spec.SecretName {
		return true
	}

	if a.Spec.IssuerRef.Name != b.Spec.IssuerRef.Name {
		return true
	}

	if a.Spec.IssuerRef.Kind != b.Spec.IssuerRef.Kind {
		return true
	}

	return false
}

func (c *Controller) setIssuerSpecificConfig(crt *v1alpha1.Certificate, issuer v1alpha1.GenericIssuer, ing *extv1beta1.Ingress, tls extv1beta1.IngressTLS) error {
	ingAnnotations := ing.Annotations
	if ingAnnotations == nil {
		ingAnnotations = map[string]string{}
	}
	// for ACME issuers
	if issuer.GetSpec().ACME != nil {
		challengeType, ok := ingAnnotations[acmeIssuerChallengeTypeAnnotation]
		if !ok {
			challengeType = c.options.DefaultACMEIssuerChallengeType
		}
		domainCfg := v1alpha1.ACMECertificateDomainConfig{
			Domains: tls.Hosts,
		}
		switch challengeType {
		case "http01":
			domainCfg.HTTP01 = &v1alpha1.ACMECertificateHTTP01Config{Ingress: ing.Name}
		case "dns01":
			dnsProvider, ok := ingAnnotations[acmeIssuerDNS01ProviderNameAnnotation]
			if !ok {
				dnsProvider = c.options.DefaultACMEIssuerDNS01ProviderName
			}
			if dnsProvider == "" {
				return fmt.Errorf("no acme issuer dns01 challenge provider specified")
			}
			domainCfg.DNS01 = &v1alpha1.ACMECertificateDNS01Config{Provider: dnsProvider}
		default:
			return fmt.Errorf("invalid acme issuer challenge type specified %q", challengeType)
		}
		crt.Spec.ACME = &v1alpha1.ACMECertificateConfig{Config: []v1alpha1.ACMECertificateDomainConfig{domainCfg}}
	}
	return nil
}

// shouldSync returns true if this ingress should have a Certificate resource
// created for it
func shouldSync(ing *extv1beta1.Ingress) bool {
	annotations := ing.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	if _, ok := annotations[issuerNameAnnotation]; ok {
		return true
	}
	if _, ok := annotations[clusterIssuerNameAnnotation]; ok {
		return true
	}
	if s, ok := annotations[tlsACMEAnnotation]; ok {
		if b, _ := strconv.ParseBool(s); b {
			return true
		}
	}
	if _, ok := annotations[acmeIssuerChallengeTypeAnnotation]; ok {
		return true
	}
	if _, ok := annotations[acmeIssuerDNS01ProviderNameAnnotation]; ok {
		return true
	}
	return false
}

// issuerForIngress will determine the issuer that should be specified on a
// Certificate created for the given Ingress resource. If one is not set, the
// default issuer given to the controller will be used.
func (c *Controller) issuerForIngress(ing *extv1beta1.Ingress) (name string, kind string) {
	name = c.options.DefaultIssuerName
	kind = c.options.DefaultIssuerKind
	annotations := ing.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	if issuerName, ok := annotations[issuerNameAnnotation]; ok {
		name = issuerName
		kind = v1alpha1.IssuerKind
	}
	if issuerName, ok := annotations[clusterIssuerNameAnnotation]; ok {
		name = issuerName
		kind = v1alpha1.ClusterIssuerKind
	}
	return name, kind
}

func (c *Controller) getGenericIssuer(namespace, name, kind string) (v1alpha1.GenericIssuer, error) {
	switch kind {
	case v1alpha1.IssuerKind:
		return c.issuerLister.Issuers(namespace).Get(name)
	case v1alpha1.ClusterIssuerKind:
		if c.clusterIssuerLister == nil {
			return nil, fmt.Errorf("cannot get ClusterIssuer for %q as ingress-shim is scoped to a single namespace", name)
		}
		return c.clusterIssuerLister.Get(name)
	default:
		return nil, fmt.Errorf(`invalid value %q for issuer kind. Must be empty, %q or %q`, kind, v1alpha1.IssuerKind, v1alpha1.ClusterIssuerKind)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"sync"
	"time"

	"github.com/haya14busa/nintendo-switch-checker/nschecker"
	"github.com/nlopes/slack"
)

var (
	interval = flag.Duration("interval", 1*time.Minute, "Check interval")
	channel  = flag.String("channel", "", "Slack channel name where checker posts comments")
	once     = flag.Bool("once", false, "Check once")
)

var sources = []nschecker.Source{
	{
		Name:          "Amazon - Nintendo Switch Joy-Con (L) / (R) グレー",
		URL:           "https://www.amazon.co.jp/%E4%BB%BB%E5%A4%A9%E5%A0%82-Nintendo-Switch-Joy-Con-%E3%82%B0%E3%83%AC%E3%83%BC/dp/B01N5QLLT3/",
		AvailableText: `この商品は、<a href="/gp/help/customer/display.html?ie=UTF8&nodeId=643004">Amazon.co.jp</a> が販売、発送します。`,
	},
	{
		Name:          "Amazon - Nintendo Switch Joy-Con (L) ネオンブルー/ (R) ネオンレッド",
		URL:           "https://www.amazon.co.jp/Nintendo-Switch-Joy-Con-%E3%83%8D%E3%82%AA%E3%83%B3%E3%83%96%E3%83%AB%E3%83%BC-%E3%83%8D%E3%82%AA%E3%83%B3%E3%83%AC%E3%83%83%E3%83%89/dp/B01NCXFWIZ/",
		AvailableText: `この商品は、<a href="/gp/help/customer/display.html?ie=UTF8&nodeId=643004">Amazon.co.jp</a> が販売、発送します。`,
	},
	{
		Name:        "My Nintendo Store",
		URL:         "https://store.nintendo.co.jp/customize.html",
		SoldOutText: `<button class="btn btn__primary_soldout to_cart" type="submit"><span>SOLD OUT</span></button>`,
	},
	{
		Name:        "Yodobashi - Nintendo Switch Joy-Con(L)/(R)グレー [Nintendo Switch本体]",
		URL:         "http://www.yodobashi.com/product/100000001003431565/",
		SoldOutText: `<div class="salesInfo"><p>予定数の販売を終了しました</p></div>`,
	},
	{
		Name:        "Yodobashi - Nintendo Switch Joy-Con(L)ネオンブルー/(R)ネオンレッド [Nintendo Switch本体]",
		URL:         "http://www.yodobashi.com/product/100000001003431566/",
		SoldOutText: `<div class="salesInfo"><p>予定数の販売を終了しました</p></div>`,
	},
	{
		Name:        "Joshin - Nintendo Switch 本体【Joy-Con(L)/(R) グレー】",
		URL:         "http://joshinweb.jp/game/40519/4902370535709.html",
		SoldOutText: `<span class="fsL"><font color="blue"><b>販売休止中です</b></font><br></span>`,
	},
	{
		Name:        "Joshin - Nintendo Switch 本体【Joy-Con(L) ネオンブルー/(R) ネオンレッド】",
		URL:         "http://joshinweb.jp/game/40519/4902370535716.html",
		SoldOutText: `<span class="fsL"><font color="blue"><b>販売休止中です</b></font><br></span>`,
	},
}

const usageMessage = "" +
	`Usage:	nintendo-switch-checker [flags]

	export SLACK_API_TOKEN=<SLACK_API_TOKEN>
`

func main() {
	flag.Usage = func() {
		fmt.Fprintln(os.Stderr, usageMessage)
		fmt.Fprintln(os.Stderr, "Flags:")
		flag.PrintDefaults()
	}
	flag.Parse()
	token := os.Getenv("SLACK_API_TOKEN")
	if token == "" {
		log.Println("Please set environment variable SLACK_API_TOKEN")
		return
	}
	if *channel == "" {
		log.Println("Please set -channel flag")
		return
	}

	c := &Checker{
		Notifier: NewNotifier(slack.New(token), *channel),
		Interval: *interval,
		Once:     *once,
	}
	if err := c.run(); err != nil {
		log.Fatal(err)
	}
}

type Checker struct {
	Notifier *Notifier
	Interval time.Duration
	Once     bool
}

func (c *Checker) run() error {
	if c.Once {
		c.runChecks()
		return nil
	}
	ticker := time.NewTicker(c.Interval)
	defer ticker.Stop()
	c.runChecks()
	for range ticker.C {
		c.runChecks()
	}
	return nil
}

func (c *Checker) runChecks() {
	log.Println("Run checkers")
	var wg sync.WaitGroup
	for _, s := range sources {
		wg.Add(1)
		go func(s nschecker.Source) {
			defer wg.Done()
			c.check(s)
		}(s)
	}
	wg.Wait()
}

func (c *Checker) check(s nschecker.Source) {
	state, err := nschecker.Check(s)
	if err != nil {
		log.Printf("Check failed: %s: %v", s.Name, err)
	}
	log.Printf("%v: %v (%s)", state, s.URL, s.Name)
	if err := c.Notifier.Notify(state, s); err != nil {
		log.Printf("fail to notify: %v", err)
	}
}

type Notifier struct {
	Cli *slack.Client

	channel string

	// url -> current state
	statesMu sync.Mutex
	states   map[string]nschecker.State
}

func NewNotifier(cli *slack.Client, channel string) *Notifier {
	return &Notifier{
		Cli:     cli,
		channel: channel,
		states:  make(map[string]nschecker.State),
	}
}

func (n *Notifier) Notify(state nschecker.State, s nschecker.Source) error {
	defer func() {
		n.statesMu.Lock()
		n.states[s.URL] = state
		n.statesMu.Unlock()
	}()
	n.statesMu.Lock()
	oldState := n.states[s.URL]
	n.statesMu.Unlock()

	if oldState == state {
		log.Printf("same state: %v url=%v name=%v", state, s.URL, s.Name)
		return nil
	}
	msg := fmt.Sprintf("%v: %v (%v)", state, s.URL, s.Name)
	params := slack.PostMessageParameters{EscapeText: false}
	_, _, err := n.Cli.PostMessage(n.channel, msg, params)
	return err
}
<commit_msg>add omni7<commit_after>package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"sync"
	"time"

	"github.com/haya14busa/nintendo-switch-checker/nschecker"
	"github.com/nlopes/slack"
)

var (
	interval = flag.Duration("interval", 1*time.Minute, "Check interval")
	channel  = flag.String("channel", "", "Slack channel name where checker posts comments")
	once     = flag.Bool("once", false, "Check once")
)

var sources = []nschecker.Source{
	{
		Name:          "Amazon - Nintendo Switch Joy-Con (L) / (R) グレー",
		URL:           "https://www.amazon.co.jp/%E4%BB%BB%E5%A4%A9%E5%A0%82-Nintendo-Switch-Joy-Con-%E3%82%B0%E3%83%AC%E3%83%BC/dp/B01N5QLLT3/",
		AvailableText: `この商品は、<a href="/gp/help/customer/display.html?ie=UTF8&nodeId=643004">Amazon.co.jp</a> が販売、発送します。`,
	},
	{
		Name:          "Amazon - Nintendo Switch Joy-Con (L) ネオンブルー/ (R) ネオンレッド",
		URL:           "https://www.amazon.co.jp/Nintendo-Switch-Joy-Con-%E3%83%8D%E3%82%AA%E3%83%B3%E3%83%96%E3%83%AB%E3%83%BC-%E3%83%8D%E3%82%AA%E3%83%B3%E3%83%AC%E3%83%83%E3%83%89/dp/B01NCXFWIZ/",
		AvailableText: `この商品は、<a href="/gp/help/customer/display.html?ie=UTF8&nodeId=643004">Amazon.co.jp</a> が販売、発送します。`,
	},
	{
		Name:        "My Nintendo Store",
		URL:         "https://store.nintendo.co.jp/customize.html",
		SoldOutText: `<button class="btn btn__primary_soldout to_cart" type="submit"><span>SOLD OUT</span></button>`,
	},
	{
		Name:        "Yodobashi - Nintendo Switch Joy-Con(L)/(R)グレー [Nintendo Switch本体]",
		URL:         "http://www.yodobashi.com/product/100000001003431565/",
		SoldOutText: `<div class="salesInfo"><p>予定数の販売を終了しました</p></div>`,
	},
	{
		Name:        "Yodobashi - Nintendo Switch Joy-Con(L)ネオンブルー/(R)ネオンレッド [Nintendo Switch本体]",
		URL:         "http://www.yodobashi.com/product/100000001003431566/",
		SoldOutText: `<div class="salesInfo"><p>予定数の販売を終了しました</p></div>`,
	},
	{
		Name:        "Joshin - Nintendo Switch 本体【Joy-Con(L)/(R) グレー】",
		URL:         "http://joshinweb.jp/game/40519/4902370535709.html",
		SoldOutText: `<span class="fsL"><font color="blue"><b>販売休止中です</b></font><br></span>`,
	},
	{
		Name:        "Joshin - Nintendo Switch 本体【Joy-Con(L) ネオンブルー/(R) ネオンレッド】",
		URL:         "http://joshinweb.jp/game/40519/4902370535716.html",
		SoldOutText: `<span class="fsL"><font color="blue"><b>販売休止中です</b></font><br></span>`,
	},
	{
		Name:        "omni7(7net) - Nintendo Switch Joy-Con (L) / (R) グレー",
		URL:         "http://7net.omni7.jp/detail/2110595636",
		SoldOutText: `<input class="linkBtn js-pressTwice" type="submit" value="在庫切れ" title="在庫切れ"`,
	},
	{
		Name:        "omni7(7net) - Nintendo Switch Joy-Con (L) ネオンブルー/ (R) ネオンレッド",
		URL:         "http://7net.omni7.jp/detail/2110595637",
		SoldOutText: `<input class="linkBtn js-pressTwice" type="submit" value="在庫切れ" title="在庫切れ"`,
	},
	{
		Name:        "omni7(iyec) - Nintendo Switch Joy-Con (L) / (R) グレー",
		URL:         "http://iyec.omni7.jp/detail/4902370535709",
		SoldOutText: `<input class="linkBtn js-pressTwice" type="submit" value="在庫切れ" title="在庫切れ"`,
	},
	{
		Name:        "omni7(iyec) - Nintendo Switch Joy-Con (L) ネオンブルー/ (R) ネオンレッド",
		URL:         "http://iyec.omni7.jp/detail/4902370535716",
		SoldOutText: `<input class="linkBtn js-pressTwice" type="submit" value="在庫切れ" title="在庫切れ"`,
	},
}

const usageMessage = "" +
	`Usage:	nintendo-switch-checker [flags]

	export SLACK_API_TOKEN=<SLACK_API_TOKEN>
`

func main() {
	flag.Usage = func() {
		fmt.Fprintln(os.Stderr, usageMessage)
		fmt.Fprintln(os.Stderr, "Flags:")
		flag.PrintDefaults()
	}
	flag.Parse()
	token := os.Getenv("SLACK_API_TOKEN")
	if token == "" {
		log.Println("Please set environment variable SLACK_API_TOKEN")
		return
	}
	if *channel == "" {
		log.Println("Please set -channel flag")
		return
	}

	c := &Checker{
		Notifier: NewNotifier(slack.New(token), *channel),
		Interval: *interval,
		Once:     *once,
	}
	if err := c.run(); err != nil {
		log.Fatal(err)
	}
}

type Checker struct {
	Notifier *Notifier
	Interval time.Duration
	Once     bool
}

func (c *Checker) run() error {
	if c.Once {
		c.runChecks()
		return nil
	}
	ticker := time.NewTicker(c.Interval)
	defer ticker.Stop()
	c.runChecks()
	for range ticker.C {
		c.runChecks()
	}
	return nil
}

func (c *Checker) runChecks() {
	log.Println("Run checkers")
	var wg sync.WaitGroup
	for _, s := range sources {
		wg.Add(1)
		go func(s nschecker.Source) {
			defer wg.Done()
			c.check(s)
		}(s)
	}
	wg.Wait()
}

func (c *Checker) check(s nschecker.Source) {
	state, err := nschecker.Check(s)
	if err != nil {
		log.Printf("Check failed: %s: %v", s.Name, err)
	}
	log.Printf("%v: %v (%s)", state, s.URL, s.Name)
	if err := c.Notifier.Notify(state, s); err != nil {
		log.Printf("fail to notify: %v", err)
	}
}

type Notifier struct {
	Cli *slack.Client

	channel string

	// url -> current state
	statesMu sync.Mutex
	states   map[string]nschecker.State
}

func NewNotifier(cli *slack.Client, channel string) *Notifier {
	return &Notifier{
		Cli:     cli,
		channel: channel,
		states:  make(map[string]nschecker.State),
	}
}

func (n *Notifier) Notify(state nschecker.State, s nschecker.Source) error {
	defer func() {
		n.statesMu.Lock()
		n.states[s.URL] = state
		n.statesMu.Unlock()
	}()
	n.statesMu.Lock()
	oldState := n.states[s.URL]
	n.statesMu.Unlock()

	if oldState == state {
		log.Printf("same state: %v url=%v name=%v", state, s.URL, s.Name)
		return nil
	}
	msg := fmt.Sprintf("%v: %v (%v)", state, s.URL, s.Name)
	params := slack.PostMessageParameters{EscapeText: false}
	_, _, err := n.Cli.PostMessage(n.channel, msg, params)
	return err
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	//"os"
	//"math/rand"
	"github.com/stephen-soltesz/go/collection"
	"github.com/stephen-soltesz/go/lineserver"
	"regexp"
	"strconv"
	"strings"
	"time"
)

var configPattern = regexp.MustCompile("axis|reset|exit|color|label")
var configError = errors.New("Matches configPattern")
var exitEarly = false

type CollectorClient struct {
	reader    *bufio.ReadWriter
	collector *collection.Collection
	axis      *collection.Axis
	line      *collection.Line
	id        int
}

func startCollectorServer(host string, port int, collector *collection.Collection) {
	addr := fmt.Sprintf("%s:%d", host, port)
	serv := lineserver.NewServer(addr)
	client_count := 0
	for {
		client_count += 1
		reader, err := serv.Accept()
		if err == io.EOF {
			break
		} else if err != nil {
			// TODO: what other errors can be handled here?
			debugLogger.Println(err)
			panic(err)
		} else if exitEarly {
			break
		}
		client := CollectorClient{}
		client.reader = reader
		client.collector = collector
		client.id = client_count
		go handleClient(&client)
	}
}

func getNextXvalue(last_value float64) float64 {
	var x float64
	if *timestamp {
		x = float64(time.Now().Unix())
	} else {
		x = last_value + 1.0
	}
	return x
}

func (client *CollectorClient) readSettings(val string) {
	fields := strings.Split(val, ":")
	if len(fields) == 1 {
		// single command
		if fields[0] == "EXIT" {
			fmt.Println("Got EXIT signal")
			exitEarly = true
			return
			//os.Exit(0)
		} else if fields[0] == "RESET" {
			fmt.Println("NOT YET SUPPORTED")
		} else {
			// unknown command
			fmt.Println("Unknown command.", fields[0])
		}
	} else if len(fields) >= 2 {
		// this is a key-value setting.
		if fields[0] == "axis" {
			debugLogger.Print("CLIENT: axis name: ", fields[1])
			client.axis = client.collector.GetAxis(fields[1])
			if len(fields) >= 4 {
				client.axis.XLabel = fields[2]
				client.axis.YLabel = fields[3]
			}
		} else if fields[0] == "label" {
			if client.axis != nil {
				debugLogger.Print("CLIENT: label name: ", fields[1])
				client.line = client.axis.GetLine(fields[1])
			}
		} else if fields[0] == "color" {
			if client.line != nil {
				debugLogger.Print("CLIENT: color: ", fields[1])
				client.line.SetColor(fields[1])
			}
		}
	}
}

func (client *CollectorClient) getNextYvalue() (float64, error) {
	val, err := client.reader.ReadString('\n')
	debugLogger.Print("CLIENT: received: ", val)
	if err != nil {
		return 0.0, err
	}
	if len(val) > 0 && !((val[0] >= '0' && val[0] <= '9') || val[0] == '.' || val[0] == '-') {
		// read settings
		debugLogger.Print("CLIENT: reading settings: ", val)
		client.readSettings(strings.TrimSpace(val))
		return client.getNextYvalue()
	} else if y, err := strconv.ParseFloat(strings.TrimSpace(val), 64); err != nil {
		ferr := err.(*strconv.NumError)
		return 0.0, ferr.Err
	} else {
		return y, nil
	}
}

func handleClient(client *CollectorClient) {
	debugLogger.Println("handleClient")

	x := 0.0
	for {
		debugLogger.Println("getting xy vals")
		x = getNextXvalue(x)
		y, err := client.getNextYvalue()
		if err == io.EOF {
			debugLogger.Println("Client EOF")
			break
		} else if err == strconv.ErrSyntax || err == strconv.ErrRange {
			// ignore parse errors.
			debugLogger.Println("Ignoring parse error:", err)
			continue
		} else if err != nil {
			// all other errors. TODO: are any fatal?
			debugLogger.Println(err)
			continue
		}
		if client.axis == nil {
			client.axis = client.collector.GetAxis("default")
		}
		if client.line == nil {
			client.line = client.axis.GetLine(fmt.Sprintf("Thread-%d", client.id))
		}
		client.line.Append(x, y)
	}
}

/*
func randRange(min, max int) float64 {
    y := float64(rand.Intn(max-min))
    return float64(min)+y
}

func newFilter(size int) (func(float64) float64) {
  b := make([]float64, size)
  i := 0
  return func(f float64) float64 {
    b[i%len(b)] = f
    i++
    sum := 0.0
    for _, value := range b {
          sum += value
    }
    return sum/float64(len(b))
  }
}

func generateData(min, max int) {
	axis := collection.Default().GetAxis("default")
	line := axis.AddLine("Thread-gen")
  filt := newFilter(3)
  count := 0.0
  for {
    //ts := float64(time.Now().Unix()/10)
    y := filt(randRange(min, max))
    // mock client: add a new point every second.
    line.Append(count, y)
    count++
    time.Sleep(time.Second)
  }
}
*/
<commit_msg>add min/max ylimits and log scale on y-axis.<commit_after>package main

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	//"os"
	//"math/rand"
	"github.com/stephen-soltesz/go/collection"
	"github.com/stephen-soltesz/go/lineserver"
	"regexp"
	"strconv"
	"strings"
	"time"
)

var configPattern = regexp.MustCompile("axis|reset|exit|color|label")
var configError = errors.New("Matches configPattern")
var exitEarly = false

type CollectorClient struct {
	reader    *bufio.ReadWriter
	collector *collection.Collection
	axis      *collection.Axis
	line      *collection.Line
	id        int
}

func startCollectorServer(host string, port int) {
	addr := fmt.Sprintf("%s:%d", host, port)
	serv := lineserver.NewServer(addr)
	client_count := 0
	for {
		client_count += 1
		reader, err := serv.Accept()
		if err == io.EOF {
			break
		} else if err != nil {
			// TODO: what other errors can be handled here?
			debugLogger.Println(err)
			panic(err)
		} else if exitEarly {
			break
		}
		client := CollectorClient{}
		client.reader = reader
		client.collector = collection.Default()
		client.id = client_count
		go handleClient(&client)
	}
}

func getNextXvalue(last_value float64) float64 {
	var x float64
	if *timestamp {
		x = float64(time.Now().Unix())
	} else {
		x = last_value + 1.0
	}
	return x
}

func (client *CollectorClient) readSettings(val string) {
	fields := strings.Split(val, ":")
	if len(fields) == 1 {
		// single command
		if fields[0] == "EXIT" {
			fmt.Println("Got EXIT signal")
			exitEarly = true
			return
			//os.Exit(0)
		} else if fields[0] == "RESET" {
			fmt.Println("NOT YET SUPPORTED")
		} else {
			// unknown command
			fmt.Println("Unknown command.", fields[0])
		}
	} else if len(fields) >= 2 {
		// this is a key-value setting.
		if fields[0] == "axis" {
			debugLogger.Print("CLIENT: axis name: ", fields[1])
			client.axis = client.collector.GetAxis(fields[1])
			if len(fields) >= 4 {
				client.axis.XLabel = fields[2]
				client.axis.YLabel = fields[3]
			}
		} else if fields[0] == "label" {
			if client.axis != nil {
				debugLogger.Print("CLIENT: label name: ", fields[1])
				client.line = client.axis.GetLine(fields[1])
			}
		} else if fields[0] == "color" {
			if client.line != nil {
				debugLogger.Print("CLIENT: color: ", fields[1])
				client.line.SetColor(fields[1])
			}
		} else if fields[0] == "yaxisscale" {
			if fields[1] == "log" {
				client.axis.Uselog = true
			}
		} else if fields[0] == "limit" {
			if len(fields) == 3 {
				client.axis.Ylimit = true
				if ymin, err := strconv.ParseFloat(strings.TrimSpace(fields[1]), 64); err == nil {
					client.axis.Ymin = ymin
				}
				if ymax, err := strconv.ParseFloat(strings.TrimSpace(fields[2]), 64); err == nil {
					client.axis.Ymax = ymax
				}
			}
		}
	}
}

func (client *CollectorClient) getNextYvalue() (float64, error) {
	val, err := client.reader.ReadString('\n')
	debugLogger.Print("CLIENT: received: ", val)
	if err != nil {
		return 0.0, err
	}
	if len(val) > 0 && !((val[0] >= '0' && val[0] <= '9') || val[0] == '.' || val[0] == '-') {
		// read settings
		debugLogger.Print("CLIENT: reading settings: ", val)
		client.readSettings(strings.TrimSpace(val))
		return client.getNextYvalue()
	} else if y, err := strconv.ParseFloat(strings.TrimSpace(val), 64); err != nil {
		ferr := err.(*strconv.NumError)
		return 0.0, ferr.Err
	} else {
		return y, nil
	}
}

func handleClient(client *CollectorClient) {
	debugLogger.Println("handleClient")

	x := 0.0
	for {
		debugLogger.Println("getting xy vals")
		x = getNextXvalue(x)
		y, err := client.getNextYvalue()
		if err == io.EOF {
			debugLogger.Println("Client EOF")
			break
		} else if err == strconv.ErrSyntax || err == strconv.ErrRange {
			// ignore parse errors.
			debugLogger.Println("Ignoring parse error:", err)
			continue
		} else if err != nil {
			// all other errors. TODO: are any fatal?
			debugLogger.Println(err)
			continue
		}
		if client.axis == nil {
			client.axis = client.collector.GetAxis("default")
		}
		if client.line == nil {
			client.line = client.axis.GetLine(fmt.Sprintf("Thread-%d", client.id))
		}
		client.line.Append(x, y)
	}
}

/*
func randRange(min, max int) float64 {
    y := float64(rand.Intn(max-min))
    return float64(min)+y
}

func newFilter(size int) (func(float64) float64) {
  b := make([]float64, size)
  i := 0
  return func(f float64) float64 {
    b[i%len(b)] = f
    i++
    sum := 0.0
    for _, value := range b {
          sum += value
    }
    return sum/float64(len(b))
  }
}

func generateData(min, max int) {
	axis := collection.Default().GetAxis("default")
	line := axis.AddLine("Thread-gen")
  filt := newFilter(3)
  count := 0.0
  for {
    //ts := float64(time.Now().Unix()/10)
    y := filt(randRange(min, max))
    // mock client: add a new point every second.
    line.Append(count, y)
    count++
    time.Sleep(time.Second)
  }
}
*/
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"net/http"
	"flag"

	"github.com/zenazn/goji"
	"github.com/zenazn/goji/web"
)

func plaintext(c web.C, w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, World!")
}

func main() {
	flag.Set("bind", ":8080")
	goji.Get("/plaintext", plaintext)
	goji.Serve()
}
<commit_msg>json tests passing<commit_after>package main

import (
	"fmt"
	"net/http"
	"encoding/json"
	"flag"

	"github.com/zenazn/goji"
	"github.com/zenazn/goji/web"
)

type Message struct {
	Message string `json:"message"`
}

// Test 1: Json Serialization
func serializeJson(c web.C, w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(&Message{"Hello, World!"})
}

// Test 6: Plaintext
func plaintext(c web.C, w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, World!")
}

func main() {
	flag.Set("bind", ":8080")
	goji.Get("/json", serializeJson)
	goji.Get("/plaintext", plaintext)
	goji.Serve()
}
<|endoftext|>


<commit_before>/*
   Copyright (C) 2014  Salsita s.r.o.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program. If not, see {http://www.gnu.org/licenses/}.
*/

package pivotal

import (
	"net/http"
	"time"
)

type Me struct {
	Id                         int        `json:"id"`
	Name                       string     `json:"name"`
	Initials                   string     `json:"initials"`
	Username                   string     `json:"username"`
	TimeZone                   *TimeZone  `json:"time_zone"`
	ApiToken                   string     `json:"api_token"`
	HasGoogleIdentity          bool       `json:"has_google_identity"`
	ProjectIds                 []int      `json:"project_ids"`
	WorkspaceIds               []int      `json:"workspace_ids"`
	Email                      string     `json:"email"`
	ReceivedInAppNotifications bool       `json:"receives_in_app_notifications"`
	CreatedAt                  *time.Time `json:"created_at"`
	UpdatedAt                  *time.Time `json:"updated_at"`
}

type MeService struct {
	client *Client
}

func newMeService(client *Client) *MeService {
	return &MeService{client}
}

func (service *MeService) Get() (*Person, *http.Response, error) {
	req, err := service.client.NewRequest("GET", "me", nil)
	if err != nil {
		return nil, nil, err
	}

	var me Person
	resp, err := service.client.Do(req, &me)
	if err != nil {
		return nil, resp, err
	}

	return &me, resp, nil
}
<commit_msg>Fix return type on me.Get()<commit_after>/*
   Copyright (C) 2014  Salsita s.r.o.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program. If not, see {http://www.gnu.org/licenses/}.
*/

package pivotal

import (
	"net/http"
	"time"
)

type Me struct {
	Id                         int        `json:"id"`
	Name                       string     `json:"name"`
	Initials                   string     `json:"initials"`
	Username                   string     `json:"username"`
	TimeZone                   *TimeZone  `json:"time_zone"`
	ApiToken                   string     `json:"api_token"`
	HasGoogleIdentity          bool       `json:"has_google_identity"`
	ProjectIds                 []int      `json:"project_ids"`
	WorkspaceIds               []int      `json:"workspace_ids"`
	Email                      string     `json:"email"`
	ReceivedInAppNotifications bool       `json:"receives_in_app_notifications"`
	CreatedAt                  *time.Time `json:"created_at"`
	UpdatedAt                  *time.Time `json:"updated_at"`
}

type MeService struct {
	client *Client
}

func newMeService(client *Client) *MeService {
	return &MeService{client}
}

func (service *MeService) Get() (*Me, *http.Response, error) {
	req, err := service.client.NewRequest("GET", "me", nil)
	if err != nil {
		return nil, nil, err
	}

	var me Me
	resp, err := service.client.Do(req, &me)
	if err != nil {
		return nil, resp, err
	}

	return &me, resp, nil
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
)

type Settings struct {
	debug    bool
	loadDb   bool
	logFile  string
	server   string
	boltFile string
	hostDb   string
	portDb   string
	nameDb   string
	hostNATS string
	portNATS string
	hostApi  string
	portApi  string
}

// Validate command-line arguments
func (settings *Settings) validateFlags() (err error) {
	var absolutePath, relPath string
	var dirInfo os.FileInfo

	// Validate Logfile
	if len(settings.logFile) != 0 {
		relPath = filepath.Dir(settings.logFile)
		if absolutePath, err = filepath.Abs(settings.logFile); err != nil {
			return fmt.Errorf("-log %s %v", settings.logFile, err)
		}
		dirInfo, err = os.Stat(relPath)
		if os.IsNotExist(err) {
			return fmt.Errorf("-log %s: directory %s %v", settings.logFile, relPath, err)
		} else if !dirInfo.IsDir() {
			return fmt.Errorf("-log %s: %s is not a directory", settings.logFile, relPath)
		}
		settings.logFile = absolutePath
	}

	// Validate BoltDb file
	if len(settings.logFile) != 0 {
		relPath = filepath.Dir(settings.logFile)
		if absolutePath, err = filepath.Abs(settings.logFile); err != nil {
			return fmt.Errorf("-logfile %s %v", settings.logFile, err)
		}
		dirInfo, err = os.Stat(relPath)
		if os.IsNotExist(err) {
			return fmt.Errorf("-logfile %s: directory %s %v", settings.logFile, relPath, err)
		} else if !dirInfo.IsDir() {
			return fmt.Errorf("-logfile %s: %s is not a directory", settings.logFile, relPath)
		}
		settings.logFile = absolutePath
	}
	return
}

func (settings *Settings) getCmdLine() (err error) {

	// Define command-line arguments
	flag.BoolVar(&settings.debug, "debug", false, "Debug logging")
	flag.BoolVar(&settings.loadDb, "dbload", false, "Load from DB instead of BoltDB")
	flag.StringVar(&settings.logFile, "log", "", "Specify logging filename")
	flag.StringVar(&settings.boltFile, "bolt", BoltDB, "Specify BoltDB filename")
	flag.StringVar(&settings.hostApi, "host", "", "Specify Api host")
	flag.StringVar(&settings.portApi, "port", "8080", "Specify Api port")
	flag.StringVar(&settings.hostDb, "dbhost", "", "Specify DB host")
	flag.StringVar(&settings.portDb, "dbport", "3306", "Specify DB port")
	flag.StringVar(&settings.hostNATS, "nhost", "", "Specify NATS host")
	flag.StringVar(&settings.portNATS, "nport", "4222", "Specify NATS port")

	// Parse commandline flag arguments
	flag.Parse()

	// Validate
	err = settings.validateFlags()
	return
}
<commit_msg>Add -apmkey flag for specifying New Relic APM license key<commit_after>package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
)

type Settings struct {
	debug    bool
	loadDb   bool
	apmKey   string
	logFile  string
	server   string
	boltFile string
	hostDb   string
	portDb   string
	nameDb   string
	hostNATS string
	portNATS string
	hostApi  string
	portApi  string
}

// Validate command-line arguments
func (settings *Settings) validateFlags() (err error) {
	var absolutePath, relPath string
	var dirInfo os.FileInfo

	// Validate Logfile
	if len(settings.logFile) > 0 {
		relPath = filepath.Dir(settings.logFile)
		if absolutePath, err = filepath.Abs(settings.logFile); err != nil {
			return fmt.Errorf("-log %s %v", settings.logFile, err)
		}
		dirInfo, err = os.Stat(relPath)
		if os.IsNotExist(err) {
			return fmt.Errorf("-log %s: directory %s %v", settings.logFile, relPath, err)
		} else if !dirInfo.IsDir() {
			return fmt.Errorf("-log %s: %s is not a directory", settings.logFile, relPath)
		}
		settings.logFile = absolutePath
	}

	// Validate BoltDb file
	if len(settings.logFile) > 0 {
		relPath = filepath.Dir(settings.logFile)
		if absolutePath, err = filepath.Abs(settings.logFile); err != nil {
			return fmt.Errorf("-logfile %s %v", settings.logFile, err)
		}
		dirInfo, err = os.Stat(relPath)
		if os.IsNotExist(err) {
			return fmt.Errorf("-logfile %s: directory %s %v", settings.logFile, relPath, err)
		} else if !dirInfo.IsDir() {
			return fmt.Errorf("-logfile %s: %s is not a directory", settings.logFile, relPath)
		}
		settings.logFile = absolutePath
	}

	// Validate APM key
	if len(settings.apmKey) > 0 {
		if len(settings.apmKey) != 40 {
			return fmt.Errorf("-apmkey: must be 40 characters")
		}
	}

	return
}

func (settings *Settings) getCmdLine() (err error) {

	// Define command-line arguments
	flag.BoolVar(&settings.debug, "debug", false, "Debug logging")
	flag.BoolVar(&settings.loadDb, "dbload", false, "Load from DB instead of BoltDB")
	flag.StringVar(&settings.apmKey, "apmkey", "", "Specify APM license key")
	flag.StringVar(&settings.logFile, "log", "", "Specify logging filename")
	flag.StringVar(&settings.boltFile, "bolt", BoltDB, "Specify BoltDB filename")
	flag.StringVar(&settings.hostApi, "host", "", "Specify Api host")
	flag.StringVar(&settings.portApi, "port", "8080", "Specify Api port")
	flag.StringVar(&settings.hostDb, "dbhost", "", "Specify DB host")
	flag.StringVar(&settings.portDb, "dbport", "3306", "Specify DB port")
	flag.StringVar(&settings.hostNATS, "nhost", "", "Specify NATS host")
	flag.StringVar(&settings.portNATS, "nport", "4222", "Specify NATS port")

	// Parse commandline flag arguments
	flag.Parse()

	// Validate
	err = settings.validateFlags()
	return
}
<|endoftext|>


<commit_before>// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package trace

import (
	"context"
	"errors"
	"log"
	"sync"
	"sync/atomic"
	"time"

	export "go.opentelemetry.io/otel/sdk/export/trace"
)

const (
	defaultMaxQueueSize       = 2048
	defaultScheduledDelay     = 5000 * time.Millisecond
	defaultMaxExportBatchSize = 512
)

var (
	errNilExporter = errors.New("exporter is nil")
)

type BatchSpanProcessorOption func(o *BatchSpanProcessorOptions)

type BatchSpanProcessorOptions struct {
	// MaxQueueSize is the maximum queue size to buffer spans for delayed processing. If the
	// queue gets full it drops the spans. Use BlockOnQueueFull to change this behavior.
	// The default value of MaxQueueSize is 2048.
	MaxQueueSize int

	// ScheduledDelayMillis is the delay interval in milliseconds between two consecutive
	// processing of batches.
	// The default value of ScheduledDelayMillis is 5000 msec.
	ScheduledDelayMillis time.Duration

	// MaxExportBatchSize is the maximum number of spans to process in a single batch.
	// If there are more than one batch worth of spans then it processes multiple batches
	// of spans one batch after the other without any delay.
	// The default value of MaxExportBatchSize is 512.
	MaxExportBatchSize int

	// BlockOnQueueFull blocks onEnd() and onStart() method if the queue is full
	// AND if BlockOnQueueFull is set to true.
	// Blocking option should be used carefully as it can severely affect the performance of an
	// application.
	BlockOnQueueFull bool
}

// BatchSpanProcessor implements SpanProcessor interfaces. It is used by
// exporters to receive export.SpanData asynchronously.
// Use BatchSpanProcessorOptions to change the behavior of the processor.
type BatchSpanProcessor struct {
	e export.SpanBatcher
	o BatchSpanProcessorOptions

	queue   chan *export.SpanData
	dropped uint32

	enqueueWait sync.WaitGroup
	stopWait    sync.WaitGroup
	stopOnce    sync.Once
	stopCh      chan struct{}
}

var _ SpanProcessor = (*BatchSpanProcessor)(nil)

// NewBatchSpanProcessor creates a new instance of BatchSpanProcessor
// for a given export. It returns an error if exporter is nil.
// The newly created BatchSpanProcessor should then be registered with sdk
// using RegisterSpanProcessor.
func NewBatchSpanProcessor(e export.SpanBatcher, opts ...BatchSpanProcessorOption) (*BatchSpanProcessor, error) {
	if e == nil {
		return nil, errNilExporter
	}

	o := BatchSpanProcessorOptions{
		ScheduledDelayMillis: defaultScheduledDelay,
		MaxQueueSize:         defaultMaxQueueSize,
		MaxExportBatchSize:   defaultMaxExportBatchSize,
	}
	for _, opt := range opts {
		opt(&o)
	}
	bsp := &BatchSpanProcessor{
		e: e,
		o: o,
	}

	bsp.queue = make(chan *export.SpanData, bsp.o.MaxQueueSize)

	bsp.stopCh = make(chan struct{})

	bsp.stopWait.Add(1)
	go func() {
		defer bsp.stopWait.Done()
		bsp.processQueue()
	}()

	return bsp, nil
}

// OnStart method does nothing.
func (bsp *BatchSpanProcessor) OnStart(sd *export.SpanData) {
}

// OnEnd method enqueues export.SpanData for later processing.
func (bsp *BatchSpanProcessor) OnEnd(sd *export.SpanData) {
	bsp.enqueue(sd)
}

// Shutdown flushes the queue and waits until all spans are processed.
// It only executes once. Subsequent call does nothing.
func (bsp *BatchSpanProcessor) Shutdown() {
	bsp.stopOnce.Do(func() {
		close(bsp.stopCh)
		bsp.stopWait.Wait()
	})
}

func WithMaxQueueSize(size int) BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.MaxQueueSize = size
	}
}

func WithMaxExportBatchSize(size int) BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.MaxExportBatchSize = size
	}
}

func WithScheduleDelayMillis(delay time.Duration) BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.ScheduledDelayMillis = delay
	}
}

func WithBlocking() BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.BlockOnQueueFull = true
	}
}

// processQueue removes spans from the `queue` channel until processor
// is shut down. It calls the exporter in batches of up to MaxExportBatchSize
// waiting up to ScheduledDelayMillis to form a batch.
func (bsp *BatchSpanProcessor) processQueue() {
	timer := time.NewTimer(bsp.o.ScheduledDelayMillis)
	defer timer.Stop()

	batch := make([]*export.SpanData, 0, bsp.o.MaxExportBatchSize)

	exportSpans := func() {
		if !timer.Stop() {
			<-timer.C
		}
		timer.Reset(bsp.o.ScheduledDelayMillis)

		if len(batch) > 0 {
			bsp.e.ExportSpans(context.Background(), batch)
			batch = batch[:0]
		}
	}

loop:
	for {
		select {
		case <-bsp.stopCh:
			break loop
		case <-timer.C:
			exportSpans()
		case sd := <-bsp.queue:
			if sd.SpanContext.IsSampled() {
				batch = append(batch, sd)
				if len(batch) == bsp.o.MaxExportBatchSize {
					exportSpans()
				}
			}
		}
	}

	go func() {
		bsp.enqueueWait.Wait()
		close(bsp.queue)
	}()

	for {
		if !timer.Stop() {
			<-timer.C
		}
		const waitTimeout = 30 * time.Second
		timer.Reset(waitTimeout)

		select {
		case sd := <-bsp.queue:
			if sd == nil {
				exportSpans()
				return
			}

			if sd.SpanContext.IsSampled() {
				batch = append(batch, sd)
				if len(batch) == bsp.o.MaxExportBatchSize {
					exportSpans()
				}
			}
		case <-timer.C:
			log.Println("bsp.enqueueWait timeout")
			exportSpans()
			return
		}
	}
}

func (bsp *BatchSpanProcessor) enqueue(sd *export.SpanData) {
	bsp.enqueueWait.Add(1)

	select {
	case <-bsp.stopCh:
		bsp.enqueueWait.Done()
		return
	default:
	}

	if bsp.o.BlockOnQueueFull {
		bsp.queue <- sd
	} else {
		select {
		case bsp.queue <- sd:
		default:
			atomic.AddUint32(&bsp.dropped, 1)
		}
	}

	bsp.enqueueWait.Done()
}
<commit_msg>Move IsSampled check<commit_after>// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package trace

import (
	"context"
	"errors"
	"log"
	"sync"
	"sync/atomic"
	"time"

	export "go.opentelemetry.io/otel/sdk/export/trace"
)

const (
	defaultMaxQueueSize       = 2048
	defaultScheduledDelay     = 5000 * time.Millisecond
	defaultMaxExportBatchSize = 512
)

var (
	errNilExporter = errors.New("exporter is nil")
)

type BatchSpanProcessorOption func(o *BatchSpanProcessorOptions)

type BatchSpanProcessorOptions struct {
	// MaxQueueSize is the maximum queue size to buffer spans for delayed processing. If the
	// queue gets full it drops the spans. Use BlockOnQueueFull to change this behavior.
	// The default value of MaxQueueSize is 2048.
	MaxQueueSize int

	// ScheduledDelayMillis is the delay interval in milliseconds between two consecutive
	// processing of batches.
	// The default value of ScheduledDelayMillis is 5000 msec.
	ScheduledDelayMillis time.Duration

	// MaxExportBatchSize is the maximum number of spans to process in a single batch.
	// If there are more than one batch worth of spans then it processes multiple batches
	// of spans one batch after the other without any delay.
	// The default value of MaxExportBatchSize is 512.
	MaxExportBatchSize int

	// BlockOnQueueFull blocks onEnd() and onStart() method if the queue is full
	// AND if BlockOnQueueFull is set to true.
	// Blocking option should be used carefully as it can severely affect the performance of an
	// application.
	BlockOnQueueFull bool
}

// BatchSpanProcessor implements SpanProcessor interfaces. It is used by
// exporters to receive export.SpanData asynchronously.
// Use BatchSpanProcessorOptions to change the behavior of the processor.
type BatchSpanProcessor struct {
	e export.SpanBatcher
	o BatchSpanProcessorOptions

	queue   chan *export.SpanData
	dropped uint32

	enqueueWait sync.WaitGroup
	stopWait    sync.WaitGroup
	stopOnce    sync.Once
	stopCh      chan struct{}
}

var _ SpanProcessor = (*BatchSpanProcessor)(nil)

// NewBatchSpanProcessor creates a new instance of BatchSpanProcessor
// for a given export. It returns an error if exporter is nil.
// The newly created BatchSpanProcessor should then be registered with sdk
// using RegisterSpanProcessor.
func NewBatchSpanProcessor(e export.SpanBatcher, opts ...BatchSpanProcessorOption) (*BatchSpanProcessor, error) {
	if e == nil {
		return nil, errNilExporter
	}

	o := BatchSpanProcessorOptions{
		ScheduledDelayMillis: defaultScheduledDelay,
		MaxQueueSize:         defaultMaxQueueSize,
		MaxExportBatchSize:   defaultMaxExportBatchSize,
	}
	for _, opt := range opts {
		opt(&o)
	}
	bsp := &BatchSpanProcessor{
		e: e,
		o: o,
	}

	bsp.queue = make(chan *export.SpanData, bsp.o.MaxQueueSize)

	bsp.stopCh = make(chan struct{})

	bsp.stopWait.Add(1)
	go func() {
		defer bsp.stopWait.Done()
		bsp.processQueue()
	}()

	return bsp, nil
}

// OnStart method does nothing.
func (bsp *BatchSpanProcessor) OnStart(sd *export.SpanData) {
}

// OnEnd method enqueues export.SpanData for later processing.
func (bsp *BatchSpanProcessor) OnEnd(sd *export.SpanData) {
	bsp.enqueue(sd)
}

// Shutdown flushes the queue and waits until all spans are processed.
// It only executes once. Subsequent call does nothing.
func (bsp *BatchSpanProcessor) Shutdown() {
	bsp.stopOnce.Do(func() {
		close(bsp.stopCh)
		bsp.stopWait.Wait()
	})
}

func WithMaxQueueSize(size int) BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.MaxQueueSize = size
	}
}

func WithMaxExportBatchSize(size int) BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.MaxExportBatchSize = size
	}
}

func WithScheduleDelayMillis(delay time.Duration) BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.ScheduledDelayMillis = delay
	}
}

func WithBlocking() BatchSpanProcessorOption {
	return func(o *BatchSpanProcessorOptions) {
		o.BlockOnQueueFull = true
	}
}

// processQueue removes spans from the `queue` channel until processor
// is shut down. It calls the exporter in batches of up to MaxExportBatchSize
// waiting up to ScheduledDelayMillis to form a batch.
func (bsp *BatchSpanProcessor) processQueue() {
	timer := time.NewTimer(bsp.o.ScheduledDelayMillis)
	defer timer.Stop()

	batch := make([]*export.SpanData, 0, bsp.o.MaxExportBatchSize)

	exportSpans := func() {
		if !timer.Stop() {
			<-timer.C
		}
		timer.Reset(bsp.o.ScheduledDelayMillis)

		if len(batch) > 0 {
			bsp.e.ExportSpans(context.Background(), batch)
			batch = batch[:0]
		}
	}

loop:
	for {
		select {
		case <-bsp.stopCh:
			break loop
		case <-timer.C:
			exportSpans()
		case sd := <-bsp.queue:
			batch = append(batch, sd)
			if len(batch) == bsp.o.MaxExportBatchSize {
				exportSpans()
			}
		}
	}

	go func() {
		bsp.enqueueWait.Wait()
		close(bsp.queue)
	}()

	for {
		if !timer.Stop() {
			<-timer.C
		}
		const waitTimeout = 30 * time.Second
		timer.Reset(waitTimeout)

		select {
		case sd := <-bsp.queue:
			if sd == nil {
				exportSpans()
				return
			}

			batch = append(batch, sd)
			if len(batch) == bsp.o.MaxExportBatchSize {
				exportSpans()
			}
		case <-timer.C:
			log.Println("bsp.enqueueWait timeout")
			exportSpans()
			return
		}
	}
}

func (bsp *BatchSpanProcessor) enqueue(sd *export.SpanData) {
	if !sd.SpanContext.IsSampled() {
		return
	}

	bsp.enqueueWait.Add(1)

	select {
	case <-bsp.stopCh:
		bsp.enqueueWait.Done()
		return
	default:
	}

	if bsp.o.BlockOnQueueFull {
		bsp.queue <- sd
	} else {
		select {
		case bsp.queue <- sd:
		default:
			atomic.AddUint32(&bsp.dropped, 1)
		}
	}

	bsp.enqueueWait.Done()
}
<|endoftext|>


<commit_before>package main

import (
	"sync"
	"time"

	"github.com/grafana/grafana/pkg/log"
)

type AggMetrics struct {
	sync.RWMutex
	Metrics        map[string]*AggMetric
	chunkSpan      uint32
	numChunks      uint32
	aggSettings    []aggSetting // for now we apply the same settings to all AggMetrics. later we may want to have different settings.
	chunkMaxStale  uint32
	metricMaxStale uint32
	maxDirtyChunks uint32
}

var totalPoints chan int

func init() {
	// measurements can lag a bit, that's ok
	totalPoints = make(chan int, 10)
}

func NewAggMetrics(chunkSpan, numChunks, chunkMaxStale, metricMaxStale uint32, maxDirtyChunks uint32, aggSettings []aggSetting) *AggMetrics {
	ms := AggMetrics{
		Metrics:        make(map[string]*AggMetric),
		chunkSpan:      chunkSpan,
		numChunks:      numChunks,
		aggSettings:    aggSettings,
		chunkMaxStale:  chunkMaxStale,
		metricMaxStale: metricMaxStale,
		maxDirtyChunks: maxDirtyChunks,
	}

	go ms.stats()
	go ms.GC()
	return &ms
}

// periodically scan chunks and close any that have not received data in a while
// TODO instrument occurences and duration of GC
func (ms *AggMetrics) GC() {
	ticker := time.Tick(time.Duration(*gcInterval) * time.Second)
	for now := range ticker {
		log.Info("checking for stale chunks that need persisting.")
		now := uint32(now.Unix())
		chunkMinTs := now - (now % ms.chunkSpan) - uint32(ms.chunkMaxStale)
		metricMinTs := now - (now % ms.chunkSpan) - uint32(ms.metricMaxStale)

		// as this is the only goroutine that can delete from ms.Metrics
		// we only need to lock long enough to get the list of actives metrics.
		// it doesnt matter if new metrics are added while we iterate this list.
		ms.RLock()
		keys := make([]string, 0, len(ms.Metrics))
		for k := range ms.Metrics {
			keys = append(keys, k)
		}
		ms.RUnlock()
		for _, key := range keys {
			ms.RLock()
			a := ms.Metrics[key]
			ms.RUnlock()
			if stale := a.GC(chunkMinTs, metricMinTs); stale {
				log.Info("metric %s is stale. Purging data from memory.", key)
				delete(ms.Metrics, key)
			}
		}

	}
}

func (ms *AggMetrics) stats() {
	for range time.Tick(time.Duration(1) * time.Second) {
		ms.RLock()
		metricsActive.Value(int64(len(ms.Metrics)))
		ms.RUnlock()
	}
}

func (ms *AggMetrics) Get(key string) (Metric, bool) {
	ms.RLock()
	m, ok := ms.Metrics[key]
	ms.RUnlock()
	return m, ok
}

func (ms *AggMetrics) GetOrCreate(key string) Metric {
	ms.Lock()
	m, ok := ms.Metrics[key]
	if !ok {
		m = NewAggMetric(key, ms.chunkSpan, ms.numChunks, ms.maxDirtyChunks, ms.aggSettings...)
		ms.Metrics[key] = m
	}
	ms.Unlock()
	return m
}
<commit_msg>race fix<commit_after>package main

import (
	"sync"
	"time"

	"github.com/grafana/grafana/pkg/log"
)

type AggMetrics struct {
	sync.RWMutex
	Metrics        map[string]*AggMetric
	chunkSpan      uint32
	numChunks      uint32
	aggSettings    []aggSetting // for now we apply the same settings to all AggMetrics. later we may want to have different settings.
	chunkMaxStale  uint32
	metricMaxStale uint32
	maxDirtyChunks uint32
}

var totalPoints chan int

func init() {
	// measurements can lag a bit, that's ok
	totalPoints = make(chan int, 10)
}

func NewAggMetrics(chunkSpan, numChunks, chunkMaxStale, metricMaxStale uint32, maxDirtyChunks uint32, aggSettings []aggSetting) *AggMetrics {
	ms := AggMetrics{
		Metrics:        make(map[string]*AggMetric),
		chunkSpan:      chunkSpan,
		numChunks:      numChunks,
		aggSettings:    aggSettings,
		chunkMaxStale:  chunkMaxStale,
		metricMaxStale: metricMaxStale,
		maxDirtyChunks: maxDirtyChunks,
	}

	go ms.stats()
	go ms.GC()
	return &ms
}

// periodically scan chunks and close any that have not received data in a while
// TODO instrument occurences and duration of GC
func (ms *AggMetrics) GC() {
	ticker := time.Tick(time.Duration(*gcInterval) * time.Second)
	for now := range ticker {
		log.Info("checking for stale chunks that need persisting.")
		now := uint32(now.Unix())
		chunkMinTs := now - (now % ms.chunkSpan) - uint32(ms.chunkMaxStale)
		metricMinTs := now - (now % ms.chunkSpan) - uint32(ms.metricMaxStale)

		// as this is the only goroutine that can delete from ms.Metrics
		// we only need to lock long enough to get the list of actives metrics.
		// it doesnt matter if new metrics are added while we iterate this list.
		ms.RLock()
		keys := make([]string, 0, len(ms.Metrics))
		for k := range ms.Metrics {
			keys = append(keys, k)
		}
		ms.RUnlock()
		for _, key := range keys {
			ms.RLock()
			a := ms.Metrics[key]
			ms.RUnlock()
			if stale := a.GC(chunkMinTs, metricMinTs); stale {
				log.Info("metric %s is stale. Purging data from memory.", key)
				ms.Lock()
				delete(ms.Metrics, key)
				ms.Unlock()
			}
		}

	}
}

func (ms *AggMetrics) stats() {
	for range time.Tick(time.Duration(1) * time.Second) {
		ms.RLock()
		metricsActive.Value(int64(len(ms.Metrics)))
		ms.RUnlock()
	}
}

func (ms *AggMetrics) Get(key string) (Metric, bool) {
	ms.RLock()
	m, ok := ms.Metrics[key]
	ms.RUnlock()
	return m, ok
}

func (ms *AggMetrics) GetOrCreate(key string) Metric {
	ms.Lock()
	m, ok := ms.Metrics[key]
	if !ok {
		m = NewAggMetric(key, ms.chunkSpan, ms.numChunks, ms.maxDirtyChunks, ms.aggSettings...)
		ms.Metrics[key] = m
	}
	ms.Unlock()
	return m
}
<|endoftext|>


<commit_before>package migration_test

import (
	"database/sql"
	"errors"

	"code.cloudfoundry.org/bbs/db/dbfakes"
	"code.cloudfoundry.org/bbs/encryption"
	"code.cloudfoundry.org/bbs/encryption/encryptionfakes"
	"code.cloudfoundry.org/bbs/migration"
	"code.cloudfoundry.org/bbs/migration/migrationfakes"
	"code.cloudfoundry.org/bbs/models"
	"code.cloudfoundry.org/clock"
	mfakes "code.cloudfoundry.org/diego-logging-client/testhelpers"
	"code.cloudfoundry.org/lager"
	"code.cloudfoundry.org/lager/lagertest"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/tedsuo/ifrit"
	"github.com/tedsuo/ifrit/ginkgomon"
)

var _ = Describe("Migration Manager", func() {
	var (
		manager          ifrit.Runner
		migrationProcess ifrit.Process

		logger *lagertest.TestLogger

		fakeSQLDB *dbfakes.FakeDB
		rawSQLDB  *sql.DB

		migrations []migration.Migration

		migrationsDone chan struct{}

		fakeMigration *migrationfakes.FakeMigration

		cryptor encryption.Cryptor

		fakeMetronClient *mfakes.FakeIngressClient
	)

	BeforeEach(func() {
		migrationsDone = make(chan struct{})

		fakeMetronClient = new(mfakes.FakeIngressClient)

		logger = lagertest.NewTestLogger("test")

		fakeSQLDB = &dbfakes.FakeDB{}

		cryptor = &encryptionfakes.FakeCryptor{}

		fakeMigration = &migrationfakes.FakeMigration{}
		migrations = []migration.Migration{fakeMigration}
	})

	JustBeforeEach(func() {
		manager = migration.NewManager(logger, fakeSQLDB, rawSQLDB, cryptor, migrations, migrationsDone, clock.NewClock(), "db-driver", fakeMetronClient)
		migrationProcess = ifrit.Background(manager)
	})

	AfterEach(func() {
		ginkgomon.Kill(migrationProcess)
	})

	Context("when configured with a SQL database", func() {
		BeforeEach(func() {
			rawSQLDB = &sql.DB{}
			fakeSQLDB.VersionReturns(&models.Version{}, nil)
		})

		It("fetches the stored version from sql", func() {
			Eventually(fakeSQLDB.VersionCallCount).Should(Equal(1))
			Consistently(fakeSQLDB.VersionCallCount).Should(Equal(1))

			ginkgomon.Interrupt(migrationProcess)
			Eventually(migrationProcess.Wait()).Should(Receive(BeNil()))
		})

		Context("when there is no version", func() {
			var (
				fakeMigrationToSQL   *migrationfakes.FakeMigration
				fakeSQLOnlyMigration *migrationfakes.FakeMigration
			)

			BeforeEach(func() {
				fakeSQLDB.VersionReturns(nil, models.ErrResourceNotFound)

				fakeMigrationToSQL = &migrationfakes.FakeMigration{}
				fakeMigrationToSQL.VersionReturns(100)

				fakeSQLOnlyMigration = &migrationfakes.FakeMigration{}
				fakeSQLOnlyMigration.VersionReturns(101)

				migrations = []migration.Migration{fakeSQLOnlyMigration, fakeMigrationToSQL}
			})

			It("runs all the migrations in the correct order and sets the version to the latest migration version", func() {
				Eventually(fakeSQLDB.SetVersionCallCount).Should(Equal(3))

				_, _, version := fakeSQLDB.SetVersionArgsForCall(0)
				Expect(version.CurrentVersion).To(BeEquivalentTo(0))

				_, _, version = fakeSQLDB.SetVersionArgsForCall(1)
				Expect(version.CurrentVersion).To(BeEquivalentTo(100))

				_, _, version = fakeSQLDB.SetVersionArgsForCall(2)
				Expect(version.CurrentVersion).To(BeEquivalentTo(101))

				Expect(fakeMigrationToSQL.UpCallCount()).To(Equal(1))
				Expect(fakeSQLOnlyMigration.UpCallCount()).To(Equal(1))
			})
		})

		Context("when fetching the version fails", func() {
			BeforeEach(func() {
				fakeSQLDB.VersionReturns(nil, errors.New("kablamo"))
			})

			It("fails early", func() {
				var err error
				Eventually(migrationProcess.Wait()).Should(Receive(&err))
				Expect(err).To(MatchError("kablamo"))
				Expect(migrationProcess.Ready()).ToNot(BeClosed())
				Expect(migrationsDone).NotTo(BeClosed())
			})
		})

		Context("when the current version is newer than bbs migration version", func() {
			BeforeEach(func() {
				fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 100}, nil)
				fakeMigration.VersionReturns(99)
			})

			It("shuts down without signalling ready", func() {
				var err error
				Eventually(migrationProcess.Wait()).Should(Receive(&err))
				Expect(err).To(MatchError("Existing DB version (100) exceeds bbs version (99)"))
				Expect(migrationProcess.Ready()).ToNot(BeClosed())
				Expect(migrationsDone).NotTo(BeClosed())
			})
		})

		Context("when the current version is the same as the bbs migration version", func() {
			BeforeEach(func() {
				fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 100}, nil)
				fakeMigration.VersionReturns(100)
			})

			It("signals ready and does not change the version", func() {
				Eventually(migrationProcess.Ready()).Should(BeClosed())
				Expect(migrationsDone).To(BeClosed())
				Consistently(fakeSQLDB.SetVersionCallCount).Should(Equal(0))
			})
		})

		Context("when the current version is older than the maximum migration version", func() {
			var fakeMigration102 *migrationfakes.FakeMigration

			BeforeEach(func() {
				fakeMigration102 = &migrationfakes.FakeMigration{}
				fakeMigration102.VersionReturns(102)

				fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 99}, nil)
				fakeMigration.VersionReturns(100)

				migrations = []migration.Migration{fakeMigration102, fakeMigration}
			})

			Describe("reporting", func() {
				It("reports the duration that it took to migrate", func() {
					Eventually(migrationProcess.Ready()).Should(BeClosed())
					Expect(migrationsDone).To(BeClosed())

					Expect(fakeMetronClient.SendDurationCallCount()).To(Equal(1))
					name, value, _ := fakeMetronClient.SendDurationArgsForCall(0)
					Expect(name).To(Equal("MigrationDuration"))
					Expect(value).NotTo(BeZero())
				})
			})

			It("sorts the migrations and runs them sequentially", func() {
				Eventually(migrationProcess.Ready()).Should(BeClosed())
				Expect(migrationsDone).To(BeClosed())
				Consistently(fakeSQLDB.SetVersionCallCount).Should(Equal(2))

				_, _, version := fakeSQLDB.SetVersionArgsForCall(0)
				Expect(version).To(Equal(&models.Version{CurrentVersion: 100}))

				_, _, version = fakeSQLDB.SetVersionArgsForCall(1)
				Expect(version).To(Equal(&models.Version{CurrentVersion: 102}))

				Expect(fakeMigration.UpCallCount()).To(Equal(1))
				Expect(fakeMigration102.UpCallCount()).To(Equal(1))
			})

			Describe("and one of the migrations takes a long time", func() {
				var longMigrationExitChan chan struct{}

				BeforeEach(func() {
					longMigrationExitChan = make(chan struct{}, 1)
					longMigration := &migrationfakes.FakeMigration{}
					longMigration.UpStub = func(logger lager.Logger) error {
						<-longMigrationExitChan
						return nil
					}
					longMigration.VersionReturns(103)
					migrations = []migration.Migration{longMigration}
				})

				AfterEach(func() {
					longMigrationExitChan <- struct{}{}
				})

				It("should not close the channel until the migration finishes", func() {
					Consistently(migrationProcess.Ready()).ShouldNot(BeClosed())
				})

				Context("when the migration finishes", func() {
					JustBeforeEach(func() {
						Eventually(longMigrationExitChan).Should(BeSent(struct{}{}))
					})

					It("should close the ready channel", func() {
						Eventually(migrationProcess.Ready()).Should(BeClosed())
					})
				})

				Context("when interrupted", func() {
					JustBeforeEach(func() {
						ginkgomon.Interrupt(migrationProcess)
					})

					It("exits and does not wait for the migration to finish", func() {
						Eventually(migrationProcess.Wait()).Should(Receive())
					})
				})
			})

			It("sets the cryptor on the migration", func() {
				Eventually(migrationProcess.Ready()).Should(BeClosed())
				Expect(migrationsDone).To(BeClosed())
				Expect(fakeMigration.SetCryptorCallCount()).To(Equal(1))
				actualCryptor := fakeMigration.SetCryptorArgsForCall(0)
				Expect(actualCryptor).To(Equal(cryptor))
			})
		})

		Context("when there are no migrations", func() {
			BeforeEach(func() {
				migrations = []migration.Migration{}
			})

			Context("and there is an existing version", func() {
				BeforeEach(func() {
					fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 100}, nil)
				})

				It("treats the bbs migration version as 0", func() {
					var err error
					Eventually(migrationProcess.Wait()).Should(Receive(&err))
					Expect(err).To(MatchError("Existing DB version (100) exceeds bbs version (0)"))
					Expect(migrationProcess.Ready()).ToNot(BeClosed())
				})
			})

			Context("and there is an existing version 0", func() {
				BeforeEach(func() {
					fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 0}, nil)
				})

				It("it skips writing the version into the db", func() {
					Consistently(fakeSQLDB.SetVersionCallCount).Should(Equal(0))
				})
			})

			Context("and there is no existing version", func() {
				BeforeEach(func() {
					fakeSQLDB.VersionReturns(nil, models.ErrResourceNotFound)
				})

				It("writes a zero version into the db", func() {
					Eventually(fakeSQLDB.SetVersionCallCount).Should(Equal(1))

					_, _, version := fakeSQLDB.SetVersionArgsForCall(0)
					Expect(version.CurrentVersion).To(BeEquivalentTo(0))
					Expect(version.CurrentVersion).To(BeEquivalentTo(0))
				})
			})
		})
	})

	Context("when not configured with a database", func() {
		BeforeEach(func() {
			rawSQLDB = nil
		})

		It("fails early", func() {
			var err error
			Eventually(migrationProcess.Wait()).Should(Receive(&err))
			Expect(err).To(MatchError("no database configured"))
			Expect(migrationProcess.Ready()).ToNot(BeClosed())
			Expect(migrationsDone).NotTo(BeClosed())
		})
	})
})
<commit_msg>Wait for migration process to exit after each test<commit_after>package migration_test

import (
	"database/sql"
	"errors"

	"code.cloudfoundry.org/bbs/db/dbfakes"
	"code.cloudfoundry.org/bbs/encryption"
	"code.cloudfoundry.org/bbs/encryption/encryptionfakes"
	"code.cloudfoundry.org/bbs/migration"
	"code.cloudfoundry.org/bbs/migration/migrationfakes"
	"code.cloudfoundry.org/bbs/models"
	"code.cloudfoundry.org/clock"
	mfakes "code.cloudfoundry.org/diego-logging-client/testhelpers"
	"code.cloudfoundry.org/lager"
	"code.cloudfoundry.org/lager/lagertest"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/tedsuo/ifrit"
	"github.com/tedsuo/ifrit/ginkgomon"
)

var _ = Describe("Migration Manager", func() {
	var (
		manager          ifrit.Runner
		migrationProcess ifrit.Process

		logger *lagertest.TestLogger

		fakeSQLDB *dbfakes.FakeDB
		rawSQLDB  *sql.DB

		migrations []migration.Migration

		migrationsDone chan struct{}

		fakeMigration *migrationfakes.FakeMigration

		cryptor encryption.Cryptor

		fakeMetronClient *mfakes.FakeIngressClient
	)

	BeforeEach(func() {
		migrationsDone = make(chan struct{})

		fakeMetronClient = new(mfakes.FakeIngressClient)

		logger = lagertest.NewTestLogger("test")

		fakeSQLDB = &dbfakes.FakeDB{}

		cryptor = &encryptionfakes.FakeCryptor{}

		fakeMigration = &migrationfakes.FakeMigration{}
		migrations = []migration.Migration{fakeMigration}
	})

	JustBeforeEach(func() {
		manager = migration.NewManager(logger, fakeSQLDB, rawSQLDB, cryptor, migrations, migrationsDone, clock.NewClock(), "db-driver", fakeMetronClient)
		migrationProcess = ifrit.Background(manager)
	})

	AfterEach(func() {
		ginkgomon.Kill(migrationProcess)
		Eventually(migrationProcess.Wait()).Should(Receive())
	})

	Context("when configured with a SQL database", func() {
		BeforeEach(func() {
			rawSQLDB = &sql.DB{}
			fakeSQLDB.VersionReturns(&models.Version{}, nil)
		})

		It("fetches the stored version from sql", func() {
			Eventually(fakeSQLDB.VersionCallCount).Should(Equal(1))
			Consistently(fakeSQLDB.VersionCallCount).Should(Equal(1))

			ginkgomon.Interrupt(migrationProcess)
			Eventually(migrationProcess.Wait()).Should(Receive(BeNil()))
		})

		Context("when there is no version", func() {
			var (
				fakeMigrationToSQL   *migrationfakes.FakeMigration
				fakeSQLOnlyMigration *migrationfakes.FakeMigration
			)

			BeforeEach(func() {
				fakeSQLDB.VersionReturns(nil, models.ErrResourceNotFound)

				fakeMigrationToSQL = &migrationfakes.FakeMigration{}
				fakeMigrationToSQL.VersionReturns(100)

				fakeSQLOnlyMigration = &migrationfakes.FakeMigration{}
				fakeSQLOnlyMigration.VersionReturns(101)

				migrations = []migration.Migration{fakeSQLOnlyMigration, fakeMigrationToSQL}
			})

			It("runs all the migrations in the correct order and sets the version to the latest migration version", func() {
				Eventually(fakeSQLDB.SetVersionCallCount).Should(Equal(3))

				_, _, version := fakeSQLDB.SetVersionArgsForCall(0)
				Expect(version.CurrentVersion).To(BeEquivalentTo(0))

				_, _, version = fakeSQLDB.SetVersionArgsForCall(1)
				Expect(version.CurrentVersion).To(BeEquivalentTo(100))

				_, _, version = fakeSQLDB.SetVersionArgsForCall(2)
				Expect(version.CurrentVersion).To(BeEquivalentTo(101))

				Expect(fakeMigrationToSQL.UpCallCount()).To(Equal(1))
				Expect(fakeSQLOnlyMigration.UpCallCount()).To(Equal(1))
			})
		})

		Context("when fetching the version fails", func() {
			BeforeEach(func() {
				fakeSQLDB.VersionReturns(nil, errors.New("kablamo"))
			})

			It("fails early", func() {
				var err error
				Eventually(migrationProcess.Wait()).Should(Receive(&err))
				Expect(err).To(MatchError("kablamo"))
				Expect(migrationProcess.Ready()).ToNot(BeClosed())
				Expect(migrationsDone).NotTo(BeClosed())
			})
		})

		Context("when the current version is newer than bbs migration version", func() {
			BeforeEach(func() {
				fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 100}, nil)
				fakeMigration.VersionReturns(99)
			})

			It("shuts down without signalling ready", func() {
				var err error
				Eventually(migrationProcess.Wait()).Should(Receive(&err))
				Expect(err).To(MatchError("Existing DB version (100) exceeds bbs version (99)"))
				Expect(migrationProcess.Ready()).ToNot(BeClosed())
				Expect(migrationsDone).NotTo(BeClosed())
			})
		})

		Context("when the current version is the same as the bbs migration version", func() {
			BeforeEach(func() {
				fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 100}, nil)
				fakeMigration.VersionReturns(100)
			})

			It("signals ready and does not change the version", func() {
				Eventually(migrationProcess.Ready()).Should(BeClosed())
				Expect(migrationsDone).To(BeClosed())
				Consistently(fakeSQLDB.SetVersionCallCount).Should(Equal(0))
			})
		})

		Context("when the current version is older than the maximum migration version", func() {
			var fakeMigration102 *migrationfakes.FakeMigration

			BeforeEach(func() {
				fakeMigration102 = &migrationfakes.FakeMigration{}
				fakeMigration102.VersionReturns(102)

				fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 99}, nil)
				fakeMigration.VersionReturns(100)

				migrations = []migration.Migration{fakeMigration102, fakeMigration}
			})

			Describe("reporting", func() {
				It("reports the duration that it took to migrate", func() {
					Eventually(migrationProcess.Ready()).Should(BeClosed())
					Expect(migrationsDone).To(BeClosed())

					Expect(fakeMetronClient.SendDurationCallCount()).To(Equal(1))
					name, value, _ := fakeMetronClient.SendDurationArgsForCall(0)
					Expect(name).To(Equal("MigrationDuration"))
					Expect(value).NotTo(BeZero())
				})
			})

			It("sorts the migrations and runs them sequentially", func() {
				Eventually(migrationProcess.Ready()).Should(BeClosed())
				Expect(migrationsDone).To(BeClosed())
				Consistently(fakeSQLDB.SetVersionCallCount).Should(Equal(2))

				_, _, version := fakeSQLDB.SetVersionArgsForCall(0)
				Expect(version).To(Equal(&models.Version{CurrentVersion: 100}))

				_, _, version = fakeSQLDB.SetVersionArgsForCall(1)
				Expect(version).To(Equal(&models.Version{CurrentVersion: 102}))

				Expect(fakeMigration.UpCallCount()).To(Equal(1))
				Expect(fakeMigration102.UpCallCount()).To(Equal(1))
			})

			Describe("and one of the migrations takes a long time", func() {
				var longMigrationExitChan chan struct{}

				BeforeEach(func() {
					longMigrationExitChan = make(chan struct{}, 1)
					longMigration := &migrationfakes.FakeMigration{}
					longMigration.UpStub = func(logger lager.Logger) error {
						<-longMigrationExitChan
						return nil
					}
					longMigration.VersionReturns(103)
					migrations = []migration.Migration{longMigration}
				})

				AfterEach(func() {
					longMigrationExitChan <- struct{}{}
				})

				It("should not close the channel until the migration finishes", func() {
					Consistently(migrationProcess.Ready()).ShouldNot(BeClosed())
				})

				Context("when the migration finishes", func() {
					JustBeforeEach(func() {
						Eventually(longMigrationExitChan).Should(BeSent(struct{}{}))
					})

					It("should close the ready channel", func() {
						Eventually(migrationProcess.Ready()).Should(BeClosed())
					})
				})

				Context("when interrupted", func() {
					JustBeforeEach(func() {
						ginkgomon.Interrupt(migrationProcess)
					})

					It("exits and does not wait for the migration to finish", func() {
						Eventually(migrationProcess.Wait()).Should(Receive())
					})
				})
			})

			It("sets the cryptor on the migration", func() {
				Eventually(migrationProcess.Ready()).Should(BeClosed())
				Expect(migrationsDone).To(BeClosed())
				Expect(fakeMigration.SetCryptorCallCount()).To(Equal(1))
				actualCryptor := fakeMigration.SetCryptorArgsForCall(0)
				Expect(actualCryptor).To(Equal(cryptor))
			})
		})

		Context("when there are no migrations", func() {
			BeforeEach(func() {
				migrations = []migration.Migration{}
			})

			Context("and there is an existing version", func() {
				BeforeEach(func() {
					fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 100}, nil)
				})

				It("treats the bbs migration version as 0", func() {
					var err error
					Eventually(migrationProcess.Wait()).Should(Receive(&err))
					Expect(err).To(MatchError("Existing DB version (100) exceeds bbs version (0)"))
					Expect(migrationProcess.Ready()).ToNot(BeClosed())
				})
			})

			Context("and there is an existing version 0", func() {
				BeforeEach(func() {
					fakeSQLDB.VersionReturns(&models.Version{CurrentVersion: 0}, nil)
				})

				It("it skips writing the version into the db", func() {
					Consistently(fakeSQLDB.SetVersionCallCount).Should(Equal(0))
				})
			})

			Context("and there is no existing version", func() {
				BeforeEach(func() {
					fakeSQLDB.VersionReturns(nil, models.ErrResourceNotFound)
				})

				It("writes a zero version into the db", func() {
					Eventually(fakeSQLDB.SetVersionCallCount).Should(Equal(1))

					_, _, version := fakeSQLDB.SetVersionArgsForCall(0)
					Expect(version.CurrentVersion).To(BeEquivalentTo(0))
					Expect(version.CurrentVersion).To(BeEquivalentTo(0))
				})
			})
		})
	})

	Context("when not configured with a database", func() {
		BeforeEach(func() {
			rawSQLDB = nil
		})

		It("fails early", func() {
			var err error
			Eventually(migrationProcess.Wait()).Should(Receive(&err))
			Expect(err).To(MatchError("no database configured"))
			Expect(migrationProcess.Ready()).ToNot(BeClosed())
			Expect(migrationsDone).NotTo(BeClosed())
		})
	})
})
<|endoftext|>


<commit_before>// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package featuregate

import (
	"flag"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestFlags(t *testing.T) {
	fs := flag.NewFlagSet("test", flag.ContinueOnError)
	Flags(fs)
	assert.Equal(t, gatesList, fs.Lookup(gatesListCfg).Value)
}

func TestGetFlags(t *testing.T) {
	assert.Equal(t, gatesList, GetFlags())
}

func TestFlagValue_basic(t *testing.T) {
	for _, tc := range []struct {
		name     string
		expected string
		input    FlagValue
	}{
		{
			name:     "single item",
			input:    FlagValue{"foo": true},
			expected: "foo",
		},
		{
			name:     "single disabled item",
			input:    FlagValue{"foo": false},
			expected: "-foo",
		},
		{
			name:     "multiple items",
			input:    FlagValue{"foo": true, "bar": false},
			expected: "-bar,foo",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			assert.Equal(t, tc.expected, tc.input.String())
			v := FlagValue{}
			assert.NoError(t, v.Set(tc.expected))
			assert.Equal(t, tc.input, v)
		})
	}
}

func TestFlagValue_SetSlice(t *testing.T) {
	for _, tc := range []struct {
		name     string
		input    []string
		expected FlagValue
	}{
		{
			name:     "single item",
			input:    []string{"foo"},
			expected: FlagValue{"foo": true},
		},
		{
			name:     "multiple items",
			input:    []string{"foo", "-bar", "+baz"},
			expected: FlagValue{"foo": true, "bar": false, "baz": true},
		},
		{
			name:     "repeated items",
			input:    []string{"foo", "-bar", "-foo"},
			expected: FlagValue{"foo": true, "bar": false},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			v := FlagValue{}
			assert.NoError(t, v.SetSlice(tc.input))
			assert.Equal(t, tc.expected, v)
		})
	}
}
<commit_msg>[service/featuregate] Add test for multiple namespaced gates with + in flags (#4966)<commit_after>// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package featuregate

import (
	"flag"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestFlags(t *testing.T) {
	fs := flag.NewFlagSet("test", flag.ContinueOnError)
	Flags(fs)
	assert.Equal(t, gatesList, fs.Lookup(gatesListCfg).Value)
}

func TestGetFlags(t *testing.T) {
	assert.Equal(t, gatesList, GetFlags())
}

func TestFlagValue_basic(t *testing.T) {
	for _, tc := range []struct {
		name     string
		expected string
		input    FlagValue
	}{
		{
			name:     "single item",
			input:    FlagValue{"foo": true},
			expected: "foo",
		},
		{
			name:     "single disabled item",
			input:    FlagValue{"foo": false},
			expected: "-foo",
		},
		{
			name:     "multiple items",
			input:    FlagValue{"foo": true, "bar": false},
			expected: "-bar,foo",
		},
		{
			name:     "multiple positive items with namespaces",
			input:    FlagValue{"foo.bar": true, "bar.baz": true},
			expected: "bar.baz,foo.bar",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			assert.Equal(t, tc.expected, tc.input.String())
			v := FlagValue{}
			assert.NoError(t, v.Set(tc.expected))
			assert.Equal(t, tc.input, v)
		})
	}
}

func TestFlagValue_withPlus(t *testing.T) {
	for _, tc := range []struct {
		name     string
		expected string
		input    FlagValue
	}{
		{
			name:     "single item",
			input:    FlagValue{"foo": true},
			expected: "+foo",
		},
		{
			name:     "multiple items",
			input:    FlagValue{"foo": true, "bar": false},
			expected: "-bar,+foo",
		},
		{
			name:     "multiple positive items with namespaces",
			input:    FlagValue{"foo.bar": true, "bar.baz": true},
			expected: "+bar.baz,+foo.bar",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			v := FlagValue{}
			assert.NoError(t, v.Set(tc.expected))
			assert.Equal(t, tc.input, v)
		})
	}
}

func TestFlagValue_SetSlice(t *testing.T) {
	for _, tc := range []struct {
		name     string
		input    []string
		expected FlagValue
	}{
		{
			name:     "single item",
			input:    []string{"foo"},
			expected: FlagValue{"foo": true},
		},
		{
			name:     "multiple items",
			input:    []string{"foo", "-bar", "+baz"},
			expected: FlagValue{"foo": true, "bar": false, "baz": true},
		},
		{
			name:     "repeated items",
			input:    []string{"foo", "-bar", "-foo"},
			expected: FlagValue{"foo": true, "bar": false},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			v := FlagValue{}
			assert.NoError(t, v.SetSlice(tc.input))
			assert.Equal(t, tc.expected, v)
		})
	}
}
<|endoftext|>