<commit_before>package api_test

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"path"

	"code.cloudfoundry.org/lager/lagertest"
	"github.com/albertoleal/requests"
	"github.com/apihub/apihub"
	"github.com/apihub/apihub/api"
	"github.com/apihub/apihub/apihubfakes"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Services", func() {
	var (
		fakeStorage *apihubfakes.FakeStorage
		tmpDir      string
		log         *lagertest.TestLogger

		apihubServer *api.ApihubServer
		server       *httptest.Server
		httpClient   requests.HTTPClient
	)

	BeforeEach(func() {
		var err error
		fakeStorage = new(apihubfakes.FakeStorage)
		log = lagertest.NewTestLogger("apihub-services-test")
		tmpDir, err = ioutil.TempDir(os.TempDir(), "apihub-server-services-test")
		socketPath := path.Join(tmpDir, fmt.Sprintf("apihub_%d.sock", GinkgoParallelNode()))

		apihubServer = api.New(log, "unix", socketPath, fakeStorage)
		Expect(err).NotTo(HaveOccurred())

		server = httptest.NewServer(apihubServer.Handler())
		httpClient = requests.NewHTTPClient(server.URL)
	})

	AfterEach(func() {
		if tmpDir != "" {
			os.RemoveAll(tmpDir)
		}
	})

	Describe("addService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{}, errors.New("service not found"))
		})

		It("adds a new service", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusCreated,
				Method:         http.MethodPost,
				Path:           "/services",
				Body:           `{"handle":"my-handle", "backends":[{"address":"http://server-a"}]}`,
			})

			Expect(stringify(body)).To(Equal(`{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://server-a","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusCreated))
			Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(1))
		})

		Context("when body is invalid", func() {
			It("returns an error and body is not json", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           "not-a-json",
				})

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":".*"}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(0))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})

			It("returns an error when missing required fields", func() {
				bdy := `{"missing":"handle"}`
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           bdy,
				})

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Handle and Backend cannot be empty."}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(0))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})
		})

		Context("when checking if there is another service for the same handle", func() {
			var reqArgs requests.Args

			BeforeEach(func() {
				reqArgs = requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           `{"handle":"my-handle", "backends":[{"address":"http://server-a"}]}`,
				}
				_, code, _, _ := httpClient.MakeRequest(reqArgs)
				Expect(code).To(Equal(http.StatusCreated))
			})

			It("returns an error when handle is already in use", func() {
				fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{Handle: "my-handle"}, nil)
				headers, code, body, _ := httpClient.MakeRequest(reqArgs)

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Handle already in use."}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(1))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})
		})

		Context("when storing a service fails", func() {
			BeforeEach(func() {
				fakeStorage.UpsertServiceReturns(errors.New("failed to store service."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           `{"handle":"my-handle", "backends":[{ "address":"http://server-a"}]}`,
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to add new service."}`))
			})
		})
	})

	Describe("listServices", func() {
		BeforeEach(func() {
			fakeStorage.ServicesReturns([]apihub.ServiceSpec{
				apihub.ServiceSpec{
					Handle: "my-handle",
					Backends: []apihub.BackendInfo{
						apihub.BackendInfo{
							Address: "http://server-a",
						},
					},
				},
			}, nil)
		})

		It("lists all existing services", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusOK,
				Method:         http.MethodGet,
				Path:           "/services",
			})

			Expect(stringify(body)).To(Equal(`{"items":[{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://server-a","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}],"item_count":1}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusOK))
			Expect(fakeStorage.ServicesCallCount()).To(Equal(1))
		})

		Context("when getting the list of services fails", func() {
			BeforeEach(func() {
				fakeStorage.ServicesReturns(nil, errors.New("failed to list services."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodGet,
					Path:           "/services",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to retrieve service list."}`))
			})
		})
	})

	Describe("removeService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{Handle: "my-handle"}, nil)
		})

		It("removes a service by handle", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusNoContent,
				Method:         http.MethodDelete,
				Path:           "/services/my-handle",
			})

			Expect(stringify(body)).To(Equal(""))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusNoContent))
			Expect(fakeStorage.RemoveServiceCallCount()).To(Equal(1))
		})

		Context("when removing a service fails", func() {
			BeforeEach(func() {
				fakeStorage.RemoveServiceReturns(errors.New("service not found"))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodDelete,
					Path:           "/services/my-bad-handle",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to remove service."}`))
			})
		})
	})

	Describe("findService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{
				Handle: "my-handle",
				Backends: []apihub.BackendInfo{
					apihub.BackendInfo{
						Address: "http://server-a",
					},
				},
			}, nil)
		})

		It("finds a service", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusOK,
				Method:         http.MethodGet,
				Path:           "/services/my-handle",
			})

			Expect(stringify(body)).To(Equal(`{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://server-a","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusOK))
			Expect(fakeStorage.FindServiceByHandleCallCount()).To(Equal(1))
		})

		Context("when finding a service fails", func() {
			BeforeEach(func() {
				fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{}, errors.New("failed to find service."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodGet,
					Path:           "/services/invalid-handle",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to find service."}`))
			})
		})
	})

	Describe("updateService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{
				Handle: "my-handle",
				Backends: []apihub.BackendInfo{
					apihub.BackendInfo{
						Address: "http://server-a",
					},
				},
			}, nil)
		})

		It("updates a service", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusOK,
				Method:         http.MethodPut,
				Path:           "/services/my-handle",
				Body:           `{"handle":"my-handle", "backends":[{"address":"http://another-server-b"}]}`,
			})

			Expect(stringify(body)).To(Equal(`{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://another-server-b","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusOK))
			Expect(fakeStorage.FindServiceByHandleCallCount()).To(Equal(1))
			Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(1))
		})

		Context("when finding a service fails", func() {
			BeforeEach(func() {
				fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{}, errors.New("failed to find service."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPut,
					Path:           "/services/my-handle",
					Body:           "{}",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to find service."}`))
			})
		})

		Context("when body is invalid", func() {
			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPut,
					Path:           "/services/my-handle",
					Body:           "not-a-json",
				})

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":".*"}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(0))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})
		})

		Context("when storing a service fails", func() {
			BeforeEach(func() {
				fakeStorage.UpsertServiceReturns(errors.New("failed to store service."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPut,
					Path:           "/services/my-handle",
					Body:           "{}",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to update service."}`))
			})
		})
	})
})
<commit_msg>Do not swallow errors in tests<commit_after>package api_test

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"path"

	"code.cloudfoundry.org/lager/lagertest"
	"github.com/albertoleal/requests"
	"github.com/apihub/apihub"
	"github.com/apihub/apihub/api"
	"github.com/apihub/apihub/apihubfakes"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Services", func() {
	var (
		fakeStorage *apihubfakes.FakeStorage
		tmpDir      string
		log         *lagertest.TestLogger

		apihubServer *api.ApihubServer
		server       *httptest.Server
		httpClient   requests.HTTPClient
	)

	BeforeEach(func() {
		var err error
		fakeStorage = new(apihubfakes.FakeStorage)
		log = lagertest.NewTestLogger("apihub-services-test")
		tmpDir, err = ioutil.TempDir(os.TempDir(), "apihub-server-services-test")
		socketPath := path.Join(tmpDir, fmt.Sprintf("apihub_%d.sock", GinkgoParallelNode()))

		apihubServer = api.New(log, "unix", socketPath, fakeStorage)
		Expect(err).NotTo(HaveOccurred())

		server = httptest.NewServer(apihubServer.Handler())
		httpClient = requests.NewHTTPClient(server.URL)
	})

	AfterEach(func() {
		if tmpDir != "" {
			os.RemoveAll(tmpDir)
		}
	})

	Describe("addService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{}, errors.New("service not found"))
		})

		It("adds a new service", func() {
			headers, code, body, err := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusCreated,
				Method:         http.MethodPost,
				Path:           "/services",
				Body:           `{"handle":"my-handle", "backends":[{"address":"http://server-a"}]}`,
			})
			Expect(err).NotTo(HaveOccurred())

			Expect(stringify(body)).To(Equal(`{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://server-a","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusCreated))
			Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(1))
		})

		Context("when body is invalid", func() {
			It("returns an error and body is not json", func() {
				headers, code, body, err := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           "not-a-json",
				})
				Expect(err).NotTo(HaveOccurred())

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":".*"}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(0))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})

			It("returns an error when missing required fields", func() {
				bdy := `{"missing":"handle"}`
				headers, code, body, err := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           bdy,
				})
				Expect(err).NotTo(HaveOccurred())

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Handle and Backend cannot be empty."}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(0))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})
		})

		Context("when checking if there is another service for the same handle", func() {
			var reqArgs requests.Args

			BeforeEach(func() {
				reqArgs = requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           `{"handle":"my-handle", "backends":[{"address":"http://server-a"}]}`,
				}
				_, code, _, err := httpClient.MakeRequest(reqArgs)
				Expect(err).NotTo(HaveOccurred())
				Expect(code).To(Equal(http.StatusCreated))
			})

			It("returns an error when handle is already in use", func() {
				fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{Handle: "my-handle"}, nil)
				headers, code, body, err := httpClient.MakeRequest(reqArgs)
				Expect(err).NotTo(HaveOccurred())

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Handle already in use."}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(1))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})
		})

		Context("when storing a service fails", func() {
			BeforeEach(func() {
				fakeStorage.UpsertServiceReturns(errors.New("failed to store service."))
			})

			It("returns an error", func() {
				headers, code, body, err := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPost,
					Path:           "/services",
					Body:           `{"handle":"my-handle", "backends":[{ "address":"http://server-a"}]}`,
				})
				Expect(err).NotTo(HaveOccurred())

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to add new service."}`))
			})
		})
	})

	Describe("listServices", func() {
		BeforeEach(func() {
			fakeStorage.ServicesReturns([]apihub.ServiceSpec{
				apihub.ServiceSpec{
					Handle: "my-handle",
					Backends: []apihub.BackendInfo{
						apihub.BackendInfo{
							Address: "http://server-a",
						},
					},
				},
			}, nil)
		})

		It("lists all existing services", func() {
			headers, code, body, err := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusOK,
				Method:         http.MethodGet,
				Path:           "/services",
			})
			Expect(err).NotTo(HaveOccurred())

			Expect(stringify(body)).To(Equal(`{"items":[{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://server-a","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}],"item_count":1}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusOK))
			Expect(fakeStorage.ServicesCallCount()).To(Equal(1))
		})

		Context("when getting the list of services fails", func() {
			BeforeEach(func() {
				fakeStorage.ServicesReturns(nil, errors.New("failed to list services."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodGet,
					Path:           "/services",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to retrieve service list."}`))
			})
		})
	})

	Describe("removeService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{Handle: "my-handle"}, nil)
		})

		It("removes a service by handle", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusNoContent,
				Method:         http.MethodDelete,
				Path:           "/services/my-handle",
			})

			Expect(stringify(body)).To(Equal(""))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusNoContent))
			Expect(fakeStorage.RemoveServiceCallCount()).To(Equal(1))
		})

		Context("when removing a service fails", func() {
			BeforeEach(func() {
				fakeStorage.RemoveServiceReturns(errors.New("service not found"))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodDelete,
					Path:           "/services/my-bad-handle",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to remove service."}`))
			})
		})
	})

	Describe("findService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{
				Handle: "my-handle",
				Backends: []apihub.BackendInfo{
					apihub.BackendInfo{
						Address: "http://server-a",
					},
				},
			}, nil)
		})

		It("finds a service", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusOK,
				Method:         http.MethodGet,
				Path:           "/services/my-handle",
			})

			Expect(stringify(body)).To(Equal(`{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://server-a","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusOK))
			Expect(fakeStorage.FindServiceByHandleCallCount()).To(Equal(1))
		})

		Context("when finding a service fails", func() {
			BeforeEach(func() {
				fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{}, errors.New("failed to find service."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodGet,
					Path:           "/services/invalid-handle",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to find service."}`))
			})
		})
	})

	Describe("updateService", func() {
		BeforeEach(func() {
			fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{
				Handle: "my-handle",
				Backends: []apihub.BackendInfo{
					apihub.BackendInfo{
						Address: "http://server-a",
					},
				},
			}, nil)
		})

		It("updates a service", func() {
			headers, code, body, _ := httpClient.MakeRequest(requests.Args{
				AcceptableCode: http.StatusOK,
				Method:         http.MethodPut,
				Path:           "/services/my-handle",
				Body:           `{"handle":"my-handle", "backends":[{"address":"http://another-server-b"}]}`,
			})

			Expect(stringify(body)).To(Equal(`{"handle":"my-handle","disabled":false,"timeout":0,"backends":[{"address":"http://another-server-b","disabled":false,"heart_beat_address":"","heart_beat_timeout":0}]}`))
			Expect(headers["Content-Type"]).To(ContainElement("application/json"))
			Expect(code).To(Equal(http.StatusOK))
			Expect(fakeStorage.FindServiceByHandleCallCount()).To(Equal(1))
			Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(1))
		})

		Context("when finding a service fails", func() {
			BeforeEach(func() {
				fakeStorage.FindServiceByHandleReturns(apihub.ServiceSpec{}, errors.New("failed to find service."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPut,
					Path:           "/services/my-handle",
					Body:           "{}",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to find service."}`))
			})
		})

		Context("when body is invalid", func() {
			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPut,
					Path:           "/services/my-handle",
					Body:           "not-a-json",
				})

				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":".*"}`))
				Expect(fakeStorage.UpsertServiceCallCount()).To(Equal(0))
				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
			})
		})

		Context("when storing a service fails", func() {
			BeforeEach(func() {
				fakeStorage.UpsertServiceReturns(errors.New("failed to store service."))
			})

			It("returns an error", func() {
				headers, code, body, _ := httpClient.MakeRequest(requests.Args{
					AcceptableCode: http.StatusBadRequest,
					Method:         http.MethodPut,
					Path:           "/services/my-handle",
					Body:           "{}",
				})

				Expect(headers["Content-Type"]).To(ContainElement("application/json"))
				Expect(code).To(Equal(http.StatusBadRequest))
				Expect(stringify(body)).To(MatchRegexp(`{"error":"bad_request","error_description":"Failed to update service."}`))
			})
		})
	})
})
<|endoftext|>


<commit_before>// Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.

package provider

import (
	"context"
	"fmt"
	"testing"

	"github.com/hashicorp/terraform/helper/resource"
	"github.com/hashicorp/terraform/terraform"
	"github.com/oracle/oci-go-sdk/common"
	oci_object_storage "github.com/oracle/oci-go-sdk/objectstorage"
)

var (
	ObjectLifecyclePolicyRequiredOnlyResource = ObjectLifecyclePolicyResourceDependencies + `
resource "oci_objectstorage_object_lifecycle_policy" "test_object_lifecycle_policy" {
	#Required
	bucket = "${oci_objectstorage_bucket.test_bucket.name}"
	namespace = "${data.oci_objectstorage_namespace.t.namespace}"
}
`

	ObjectLifecyclePolicyResourceConfig = ObjectLifecyclePolicyResourceDependencies + `
resource "oci_objectstorage_object_lifecycle_policy" "test_object_lifecycle_policy" {
	#Required
	bucket = "${oci_objectstorage_bucket.test_bucket.name}"
	namespace = "${data.oci_objectstorage_namespace.t.namespace}"

	#Optional
	rules {
		#Required
		action = "${var.object_lifecycle_policy_rules_action}"
		is_enabled = "${var.object_lifecycle_policy_rules_is_enabled}"
		name = "${var.object_lifecycle_policy_rules_name}"
		time_amount = "${var.object_lifecycle_policy_rules_time_amount}"
		time_unit = "${var.object_lifecycle_policy_rules_time_unit}"

		#Optional
		object_name_filter {

			#Optional
			inclusion_prefixes = "${var.object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes}"
		}
	}
}
`
	ObjectLifecyclePolicyPropertyVariables = `
variable "object_lifecycle_policy_bucket" { default = "my-test-1" }
variable "object_lifecycle_policy_namespace" { default = "namespace" }
variable "object_lifecycle_policy_rules_action" { default = "ARCHIVE" }
variable "object_lifecycle_policy_rules_is_enabled" { default = false }
variable "object_lifecycle_policy_rules_name" { default = "sampleRule" }
variable "object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes" { default = ["my-test-1","my-test-2"] }
variable "object_lifecycle_policy_rules_time_amount" { default = 10 }
variable "object_lifecycle_policy_rules_time_unit" { default = "DAYS" }

`
	ObjectLifecyclePolicyResourceDependencies = BucketRequiredOnlyResource
)

func TestObjectStorageObjectLifecyclePolicyResource_basic(t *testing.T) {
	provider := testAccProvider
	config := testProviderConfig()

	compartmentId := getEnvSettingWithBlankDefault("compartment_ocid")
	compartmentIdVariableStr := fmt.Sprintf("variable \"compartment_id\" { default = \"%s\" }\n", compartmentId)

	resourceName := "oci_objectstorage_object_lifecycle_policy.test_object_lifecycle_policy"

	singularDatasourceName := "data.oci_objectstorage_object_lifecycle_policy.test_object_lifecycle_policy"

	var resId, resId2 string

	resource.Test(t, resource.TestCase{
		PreCheck: func() { testAccPreCheck(t) },
		Providers: map[string]terraform.ResourceProvider{
			"oci": provider,
		},
		CheckDestroy: testAccCheckObjectStorageObjectLifecyclePolicyDestroy,
		Steps: []resource.TestStep{
			// verify create
			{
				Config: config + ObjectLifecyclePolicyPropertyVariables + compartmentIdVariableStr + ObjectLifecyclePolicyRequiredOnlyResource,
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "my-test-1"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "0"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),

					func(s *terraform.State) (err error) {
						resId, err = fromInstanceState(s, resourceName, "id")
						return err
					},
				),
			},

			// delete before next create
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies,
			},
			// verify create with optionals
			{
				Config: config + ObjectLifecyclePolicyPropertyVariables + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig,
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "my-test-1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(resourceName, "rules", map[string]string{
						"action":                                    "ARCHIVE",
						"is_enabled":                                "false",
						"name":                                      "sampleRule",
						"object_name_filter.#":                      "1",
						"object_name_filter.0.inclusion_prefixes.#": "2",
						"time_amount":                               "10",
						"time_unit":                                 "DAYS",
					},
						[]string{}),

					func(s *terraform.State) (err error) {
						resId, err = fromInstanceState(s, resourceName, "id")
						return err
					},
				),
			},

			// verify updates to updatable parameters
			{
				Config: config + `
variable "object_lifecycle_policy_bucket" { default = "my-test-1" }
variable "object_lifecycle_policy_namespace" { default = "namespace" }
variable "object_lifecycle_policy_rules_action" { default = "DELETE" }
variable "object_lifecycle_policy_rules_is_enabled" { default = true }
variable "object_lifecycle_policy_rules_name" { default = "name2" }
variable "object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes" { default = ["my-test-1","my-test-2","my-test-3"] }
variable "object_lifecycle_policy_rules_time_amount" { default = 11 }
variable "object_lifecycle_policy_rules_time_unit" { default = "YEARS" }

                ` + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig,
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "my-test-1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(resourceName, "rules", map[string]string{
						"action":                                    "DELETE",
						"is_enabled":                                "true",
						"name":                                      "name2",
						"object_name_filter.#":                      "1",
						"object_name_filter.0.inclusion_prefixes.#": "3",
						"time_amount":                               "11",
						"time_unit":                                 "YEARS",
					},
						[]string{}),

					func(s *terraform.State) (err error) {
						resId2, err = fromInstanceState(s, resourceName, "id")
						if resId != resId2 {
							return fmt.Errorf("Resource recreated when it was supposed to be updated.")
						}
						return err
					},
				),
			},
			// verify singular datasource
			{
				Config: config + `
variable "object_lifecycle_policy_bucket" { default = "my-test-1" }
variable "object_lifecycle_policy_namespace" { default = "namespace" }
variable "object_lifecycle_policy_rules_action" { default = "DELETE" }
variable "object_lifecycle_policy_rules_is_enabled" { default = true }
variable "object_lifecycle_policy_rules_name" { default = "name2" }
variable "object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes" { default = ["my-test-1","my-test-2","my-test-3"] }
variable "object_lifecycle_policy_rules_time_amount" { default = 11 }
variable "object_lifecycle_policy_rules_time_unit" { default = "YEARS" }

data "oci_objectstorage_object_lifecycle_policy" "test_object_lifecycle_policy" {
	#Required
	bucket = "${var.object_lifecycle_policy_bucket}"
	namespace = "${data.oci_objectstorage_namespace.t.namespace}"
}
                ` + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig,
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(singularDatasourceName, "bucket", "my-test-1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),
					resource.TestCheckResourceAttr(singularDatasourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(singularDatasourceName, "rules", map[string]string{},
						[]string{}),

					resource.TestCheckResourceAttr(singularDatasourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(singularDatasourceName, "rules", map[string]string{
						"action":                                    "DELETE",
						"is_enabled":                                "true",
						"name":                                      "name2",
						"object_name_filter.#":                      "1",
						"object_name_filter.0.inclusion_prefixes.#": "3",
						"time_amount":                               "11",
						"time_unit":                                 "YEARS",
					},
						[]string{}),
					resource.TestCheckResourceAttrSet(singularDatasourceName, "time_created"),
				),
			},
			// remove singular datasource from previous step so that it doesn't conflict with import tests
			{
				Config: config + `
variable "object_lifecycle_policy_bucket" { default = "my-test-1" }
variable "object_lifecycle_policy_namespace" { default = "namespace" }
variable "object_lifecycle_policy_rules_action" { default = "DELETE" }
variable "object_lifecycle_policy_rules_is_enabled" { default = true }
variable "object_lifecycle_policy_rules_name" { default = "name2" }
variable "object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes" { default = ["my-test-1","my-test-2","my-test-3"] }
variable "object_lifecycle_policy_rules_time_amount" { default = 11 }
variable "object_lifecycle_policy_rules_time_unit" { default = "YEARS" }

                ` + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig,
			},
			// verify resource import
			{
				Config:                  config,
				ImportState:             true,
				ImportStateVerify:       true,
				ImportStateVerifyIgnore: []string{},
				ResourceName:            resourceName,
			},
		},
	})
}

func TestObjectStorageObjectLifecyclePolicyResource_validations(t *testing.T) {
	provider := testAccProvider
	config := testProviderConfig()

	compartmentId := getEnvSettingWithBlankDefault("compartment_ocid")
	compartmentIdVariableStr := fmt.Sprintf("variable \"compartment_id\" { default = \"%s\" }\n", compartmentId)

	resourceName := "oci_objectstorage_object_lifecycle_policy.test_object_lifecycle_policy"

	resource.Test(t, resource.TestCase{
		PreCheck: func() { testAccPreCheck(t) },
		Providers: map[string]terraform.ResourceProvider{
			"oci": provider,
		},
		CheckDestroy: testAccCheckObjectStorageObjectLifecyclePolicyDestroy,
		Steps: []resource.TestStep{
			// verify baseline create
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig + `
variable "object_lifecycle_policy_bucket" { default = "my-test-1" }
variable "object_lifecycle_policy_namespace" { default = "namespace" }
variable "object_lifecycle_policy_rules_action" { default = "ARCHIVE" }
variable "object_lifecycle_policy_rules_is_enabled" { default = false }
variable "object_lifecycle_policy_rules_name" { default = "sampleRule" }
variable "object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes" { default = ["my-test-1","my-test-2"] }
variable "object_lifecycle_policy_rules_time_amount" { default = 10 }
variable "object_lifecycle_policy_rules_time_unit" { default = "DAYS" }
`,
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "my-test-1"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),

					func(s *terraform.State) (err error) {
						_, err = fromInstanceState(s, resourceName, "id")
						return err
					},
				),
			},
			// change order of inclusion prefixes
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig + `
variable "object_lifecycle_policy_bucket" { default = "my-test-1" }
variable "object_lifecycle_policy_namespace" { default = "namespace" }
variable "object_lifecycle_policy_rules_action" { default = "ARCHIVE" }
variable "object_lifecycle_policy_rules_is_enabled" { default = false }
variable "object_lifecycle_policy_rules_name" { default = "sampleRule" }
variable "object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes" { default = ["my-test-2", "my-test-1"] }
variable "object_lifecycle_policy_rules_time_amount" { default = 10 }
variable "object_lifecycle_policy_rules_time_unit" { default = "DAYS" }
`,
				PlanOnly:           true,
				ExpectNonEmptyPlan: false,
			},
			// Remove inclusion prefixes to see plan has changed
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig + `
variable "object_lifecycle_policy_bucket" { default = "my-test-1" }
variable "object_lifecycle_policy_namespace" { default = "namespace" }
variable "object_lifecycle_policy_rules_action" { default = "ARCHIVE" }
variable "object_lifecycle_policy_rules_is_enabled" { default = false }
variable "object_lifecycle_policy_rules_name" { default = "sampleRule" }
variable "object_lifecycle_policy_rules_object_name_filter_inclusion_prefixes" { default = [] }
variable "object_lifecycle_policy_rules_time_amount" { default = 10 }
variable "object_lifecycle_policy_rules_time_unit" { default = "DAYS" }
`,
				PlanOnly:           true,
				ExpectNonEmptyPlan: true,
			},
		},
	})
}

func testAccCheckObjectStorageObjectLifecyclePolicyDestroy(s *terraform.State) error {
	noResourceFound := true
	client := testAccProvider.Meta().(*OracleClients).objectStorageClient
	for _, rs := range s.RootModule().Resources {
		if rs.Type == "oci_objectstorage_object_lifecycle_policy" {
			noResourceFound = false
			request := oci_object_storage.GetObjectLifecyclePolicyRequest{}

			if value, ok := rs.Primary.Attributes["bucket"]; ok {
				request.BucketName = &value
			}

			if value, ok := rs.Primary.Attributes["namespace"]; ok {
				request.NamespaceName = &value
			}

			_, err := client.GetObjectLifecyclePolicy(context.Background(), request)

			if err == nil {
				return fmt.Errorf("resource still exists")
			}

			//Verify that exception is for '404 not found'.
			if failure, isServiceError := common.IsServiceError(err); !isServiceError || failure.GetHTTPStatusCode() != 404 {
				return err
			}
		}
	}
	if noResourceFound {
		return fmt.Errorf("at least one resource was expected from the state file, but could not be found")
	}

	return nil
}

func initObjectStorageObjectLifecyclePolicySweeper() {
	resource.AddTestSweepers("ObjectStorageObjectLifecyclePolicy", &resource.Sweeper{
		Name:         "ObjectStorageObjectLifecyclePolicy",
		Dependencies: DependencyGraph["objectLifecyclePolicy"],
		F:            sweepObjectStorageObjectLifecyclePolicyResource,
	})
}

func sweepObjectStorageObjectLifecyclePolicyResource(compartment string) error {
	return nil
}
<commit_msg>Update TestObjectStorageObjectLifecyclePolicyResource_validations test to use custom bucket<commit_after>// Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.

package provider

import (
	"context"
	"fmt"
	"testing"

	"github.com/hashicorp/terraform/helper/resource"
	"github.com/hashicorp/terraform/terraform"
	"github.com/oracle/oci-go-sdk/common"
	oci_object_storage "github.com/oracle/oci-go-sdk/objectstorage"
)

var (
	ObjectLifecyclePolicyRequiredOnlyResource = ObjectLifecyclePolicyResourceDependencies +
		generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Required, Create, objectLifecyclePolicyRepresentation)

	ObjectLifecyclePolicyResourceConfig = ObjectLifecyclePolicyResourceDependencies +
		generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Optional, Update, objectLifecyclePolicyRepresentation)

	objectLifecyclePolicySingularDataSourceRepresentation = map[string]interface{}{
		"bucket":    Representation{repType: Required, create: `lifecycle-policy-test-1`},
		"namespace": Representation{repType: Required, create: `${data.oci_objectstorage_namespace.t.namespace}`},
	}

	objectLifecyclePolicyRepresentation = map[string]interface{}{
		"bucket":    Representation{repType: Required, create: `lifecycle-policy-test-1`},
		"namespace": Representation{repType: Required, create: `${data.oci_objectstorage_namespace.t.namespace}`},
		"rules":     RepresentationGroup{Optional, objectLifecyclePolicyRulesRepresentation},
	}
	objectLifecyclePolicyRulesRepresentation = map[string]interface{}{
		"action":             Representation{repType: Required, create: `ARCHIVE`, update: `DELETE`},
		"is_enabled":         Representation{repType: Required, create: `false`, update: `true`},
		"name":               Representation{repType: Required, create: `sampleRule`, update: `name2`},
		"time_amount":        Representation{repType: Required, create: `10`, update: `11`},
		"time_unit":          Representation{repType: Required, create: `DAYS`, update: `YEARS`},
		"object_name_filter": RepresentationGroup{Optional, objectLifecyclePolicyRulesObjectNameFilterRepresentation},
	}
	objectLifecyclePolicyRulesObjectNameFilterRepresentation = map[string]interface{}{
		"inclusion_prefixes": Representation{repType: Optional, create: []string{`lifecycle-policy-test-1`, `lifecycle-policy-test-2`}, update: []string{`lifecycle-policy-test-1`, `lifecycle-policy-test-2`, `lifecycle-policy-test-3`}},
	}

	ObjectLifecyclePolicyResourceDependencies = BucketResourceDependencies +
		generateResourceFromRepresentationMap("oci_objectstorage_bucket", "test_bucket", Required, Create,
			getUpdatedRepresentationCopy("name", Representation{repType: Required, create: `lifecycle-policy-test-1`}, bucketRepresentation))
)

func TestObjectStorageObjectLifecyclePolicyResource_basic(t *testing.T) {
	provider := testAccProvider
	config := testProviderConfig()

	compartmentId := getEnvSettingWithBlankDefault("compartment_ocid")
	compartmentIdVariableStr := fmt.Sprintf("variable \"compartment_id\" { default = \"%s\" }\n", compartmentId)

	resourceName := "oci_objectstorage_object_lifecycle_policy.test_object_lifecycle_policy"

	singularDatasourceName := "data.oci_objectstorage_object_lifecycle_policy.test_object_lifecycle_policy"

	var resId, resId2 string

	resource.Test(t, resource.TestCase{
		PreCheck: func() { testAccPreCheck(t) },
		Providers: map[string]terraform.ResourceProvider{
			"oci": provider,
		},
		CheckDestroy: testAccCheckObjectStorageObjectLifecyclePolicyDestroy,
		Steps: []resource.TestStep{
			// verify create
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies +
					generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Required, Create, objectLifecyclePolicyRepresentation),
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "lifecycle-policy-test-1"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "0"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),

					func(s *terraform.State) (err error) {
						resId, err = fromInstanceState(s, resourceName, "id")
						return err
					},
				),
			},

			// delete before next create
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies,
			},
			// verify create with optionals
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies +
					generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Optional, Create, objectLifecyclePolicyRepresentation),
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "lifecycle-policy-test-1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(resourceName, "rules", map[string]string{
						"action":                                    "ARCHIVE",
						"is_enabled":                                "false",
						"name":                                      "sampleRule",
						"object_name_filter.#":                      "1",
						"object_name_filter.0.inclusion_prefixes.#": "2",
						"time_amount":                               "10",
						"time_unit":                                 "DAYS",
					},
						[]string{}),

					func(s *terraform.State) (err error) {
						resId, err = fromInstanceState(s, resourceName, "id")
						return err
					},
				),
			},

			// verify updates to updatable parameters
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies +
					generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Optional, Update, objectLifecyclePolicyRepresentation),
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "lifecycle-policy-test-1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(resourceName, "rules", map[string]string{
						"action":                                    "DELETE",
						"is_enabled":                                "true",
						"name":                                      "name2",
						"object_name_filter.#":                      "1",
						"object_name_filter.0.inclusion_prefixes.#": "3",
						"time_amount":                               "11",
						"time_unit":                                 "YEARS",
					},
						[]string{}),

					func(s *terraform.State) (err error) {
						resId2, err = fromInstanceState(s, resourceName, "id")
						if resId != resId2 {
							return fmt.Errorf("Resource recreated when it was supposed to be updated.")
						}
						return err
					},
				),
			},
			// verify singular datasource
			{
				Config: config +
					generateDataSourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Required, Create, objectLifecyclePolicySingularDataSourceRepresentation) +
					compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig,
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(singularDatasourceName, "bucket", "lifecycle-policy-test-1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),
					resource.TestCheckResourceAttr(singularDatasourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(singularDatasourceName, "rules", map[string]string{},
						[]string{}),

					resource.TestCheckResourceAttr(singularDatasourceName, "rules.#", "1"),
					CheckResourceSetContainsElementWithProperties(singularDatasourceName, "rules", map[string]string{
						"action":                                    "DELETE",
						"is_enabled":                                "true",
						"name":                                      "name2",
						"object_name_filter.#":                      "1",
						"object_name_filter.0.inclusion_prefixes.#": "3",
						"time_amount":                               "11",
						"time_unit":                                 "YEARS",
					},
						[]string{}),
					resource.TestCheckResourceAttrSet(singularDatasourceName, "time_created"),
				),
			},
			// remove singular datasource from previous step so that it doesn't conflict with import tests
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceConfig,
			},
			// verify resource import
			{
				Config:                  config,
				ImportState:             true,
				ImportStateVerify:       true,
				ImportStateVerifyIgnore: []string{},
				ResourceName:            resourceName,
			},
		},
	})
}

func TestObjectStorageObjectLifecyclePolicyResource_validations(t *testing.T) {
	provider := testAccProvider
	config := testProviderConfig()

	compartmentId := getEnvSettingWithBlankDefault("compartment_ocid")
	compartmentIdVariableStr := fmt.Sprintf("variable \"compartment_id\" { default = \"%s\" }\n", compartmentId)

	resourceName := "oci_objectstorage_object_lifecycle_policy.test_object_lifecycle_policy"

	resource.Test(t, resource.TestCase{
		PreCheck: func() { testAccPreCheck(t) },
		Providers: map[string]terraform.ResourceProvider{
			"oci": provider,
		},
		CheckDestroy: testAccCheckObjectStorageObjectLifecyclePolicyDestroy,
		Steps: []resource.TestStep{
			// verify baseline create
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies +
					generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Optional, Create, objectLifecyclePolicyRepresentation),
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr(resourceName, "bucket", "lifecycle-policy-test-1"),
					resource.TestCheckResourceAttr(resourceName, "rules.#", "1"),
					resource.TestCheckResourceAttrSet(resourceName, "namespace"),

					func(s *terraform.State) (err error) {
						_, err = fromInstanceState(s, resourceName, "id")
						return err
					},
				),
			},
			// change order of inclusion prefixes
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies +
					generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Optional, Create,
						getUpdatedRepresentationCopy("rules.object_name_filter.inclusion_prefixes", Representation{repType: Optional, create: []string{`lifecycle-policy-test-2`, `lifecycle-policy-test-1`}}, objectLifecyclePolicyRepresentation)),
				PlanOnly:           true,
				ExpectNonEmptyPlan: false,
			},
			// Remove inclusion prefixes to see plan has changed
			{
				Config: config + compartmentIdVariableStr + ObjectLifecyclePolicyResourceDependencies +
					generateResourceFromRepresentationMap("oci_objectstorage_object_lifecycle_policy", "test_object_lifecycle_policy", Optional, Create,
						getUpdatedRepresentationCopy("rules.object_name_filter.inclusion_prefixes", Representation{repType: Optional, create: []string{}}, objectLifecyclePolicyRepresentation)),
				PlanOnly:           true,
				ExpectNonEmptyPlan: true,
			},
		},
	})
}

func testAccCheckObjectStorageObjectLifecyclePolicyDestroy(s *terraform.State) error {
	noResourceFound := true
	client := testAccProvider.Meta().(*OracleClients).objectStorageClient
	for _, rs := range s.RootModule().Resources {
		if rs.Type == "oci_objectstorage_object_lifecycle_policy" {
			noResourceFound = false
			request := oci_object_storage.GetObjectLifecyclePolicyRequest{}

			if value, ok := rs.Primary.Attributes["bucket"]; ok {
				request.BucketName = &value
			}

			if value, ok := rs.Primary.Attributes["namespace"]; ok {
				request.NamespaceName = &value
			}

			_, err := client.GetObjectLifecyclePolicy(context.Background(), request)

			if err == nil {
				return fmt.Errorf("resource still exists")
			}

			//Verify that exception is for '404 not found'.
			if failure, isServiceError := common.IsServiceError(err); !isServiceError || failure.GetHTTPStatusCode() != 404 {
				return err
			}
		}
	}
	if noResourceFound {
		return fmt.Errorf("at least one resource was expected from the state file, but could not be found")
	}

	return nil
}

func initObjectStorageObjectLifecyclePolicySweeper() {
	resource.AddTestSweepers("ObjectStorageObjectLifecyclePolicy", &resource.Sweeper{
		Name:         "ObjectStorageObjectLifecyclePolicy",
		Dependencies: DependencyGraph["objectLifecyclePolicy"],
		F:            sweepObjectStorageObjectLifecyclePolicyResource,
	})
}

func sweepObjectStorageObjectLifecyclePolicyResource(compartment string) error {
	return nil
}
<|endoftext|>


<commit_before>// Copyright © 2017 The Things Network
// Use of this source code is governed by the MIT license that can be found in the LICENSE file.

package amqp

import (
	"os"
	"testing"
	"time"

	"sync"

	"github.com/TheThingsNetwork/ttn/core/types"
	. "github.com/smartystreets/assertions"
	AMQP "github.com/streadway/amqp"
)

var host string

func init() {
	host = os.Getenv("AMQP_ADDRESS")
	if host == "" {
		host = "localhost:5672"
	}
}

func TestNewClient(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestNewClient"), "guest", "guest", host)
	a.So(c, ShouldNotBeNil)
}

func TestConnect(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestConnect"), "guest", "guest", host)
	err := c.Connect()
	defer c.Disconnect()
	a.So(err, ShouldBeNil)

	// Connecting while already connected should not change anything
	err = c.Connect()
	defer c.Disconnect()
	a.So(err, ShouldBeNil)
}

func TestConnectInvalidAddress(t *testing.T) {
	a := New(t)
	ConnectRetries = 2
	ConnectRetryDelay = 50 * time.Millisecond
	c := NewClient(getLogger(t, "TestConnectInvalidAddress"), "guest", "guest", "localhost:56720")
	err := c.Connect()
	defer c.Disconnect()
	a.So(err, ShouldNotBeNil)
}

func TestIsConnected(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestIsConnected"), "guest", "guest", host)

	a.So(c.IsConnected(), ShouldBeFalse)

	c.Connect()
	defer c.Disconnect()

	a.So(c.IsConnected(), ShouldBeTrue)
}

func TestDisconnect(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestDisconnect"), "guest", "guest", host)

	// Disconnecting when not connected should not change anything
	c.Disconnect()
	a.So(c.IsConnected(), ShouldBeFalse)

	c.Connect()
	defer c.Disconnect()
	c.Disconnect()

	a.So(c.IsConnected(), ShouldBeFalse)
}

func TestReopenChannelClient(t *testing.T) {
	a := New(t)
	ctx := getLogger(t, "TestReopenChannelClient")
	c := NewClient(ctx, "guest", "guest", host).(*DefaultClient)
	closed, err := c.connect(false)
	a.So(err, ShouldBeNil)
	defer c.Disconnect()

	publisher := c.NewPublisher("amq.topic")
	err = publisher.Open()
	a.So(err, ShouldBeNil)
	defer publisher.Close()

	subscriber := c.NewSubscriber("amq.topic", "", false, true)
	err = subscriber.Open()
	a.So(err, ShouldBeNil)
	defer subscriber.Close()

	wg := sync.WaitGroup{}
	err = subscriber.SubscribeDownlink(func(_ Subscriber, appID string, _ string, _ types.DownlinkMessage) {
		a.So(appID, ShouldEqual, "app")
		ctx.Debugf("Got downlink message")
		wg.Done()
	})
	a.So(err, ShouldBeNil)

	test := func() error {
		ctx.Debug("Testing publish")
		return publisher.PublishDownlink(types.DownlinkMessage{
			AppID: "app",
		})
	}

	// First attempt should be OK
	wg.Add(1)
	err = test()
	a.So(err, ShouldBeNil)
	wg.Wait()

	// Make sure that the old channel is closed
	publisher.(*DefaultPublisher).channel.Close()

	// Simulate a connection close so a new channel should be opened
	closed <- AMQP.ErrClosed

	// Give the reconnect some time
	time.Sleep(100 * time.Millisecond)

	// Second attempt should be OK as well and will only work on a new channel
	err = test()
	a.So(err, ShouldBeNil)
}
<commit_msg>Reproduced subscription loss after reopening channel<commit_after>// Copyright © 2017 The Things Network
// Use of this source code is governed by the MIT license that can be found in the LICENSE file.

package amqp

import (
	"os"
	"testing"
	"time"

	"github.com/TheThingsNetwork/ttn/core/types"
	. "github.com/smartystreets/assertions"
	AMQP "github.com/streadway/amqp"
)

var host string

func init() {
	host = os.Getenv("AMQP_ADDRESS")
	if host == "" {
		host = "localhost:5672"
	}
}

func TestNewClient(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestNewClient"), "guest", "guest", host)
	a.So(c, ShouldNotBeNil)
}

func TestConnect(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestConnect"), "guest", "guest", host)
	err := c.Connect()
	defer c.Disconnect()
	a.So(err, ShouldBeNil)

	// Connecting while already connected should not change anything
	err = c.Connect()
	defer c.Disconnect()
	a.So(err, ShouldBeNil)
}

func TestConnectInvalidAddress(t *testing.T) {
	a := New(t)
	ConnectRetries = 2
	ConnectRetryDelay = 50 * time.Millisecond
	c := NewClient(getLogger(t, "TestConnectInvalidAddress"), "guest", "guest", "localhost:56720")
	err := c.Connect()
	defer c.Disconnect()
	a.So(err, ShouldNotBeNil)
}

func TestIsConnected(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestIsConnected"), "guest", "guest", host)

	a.So(c.IsConnected(), ShouldBeFalse)

	c.Connect()
	defer c.Disconnect()

	a.So(c.IsConnected(), ShouldBeTrue)
}

func TestDisconnect(t *testing.T) {
	a := New(t)
	c := NewClient(getLogger(t, "TestDisconnect"), "guest", "guest", host)

	// Disconnecting when not connected should not change anything
	c.Disconnect()
	a.So(c.IsConnected(), ShouldBeFalse)

	c.Connect()
	defer c.Disconnect()
	c.Disconnect()

	a.So(c.IsConnected(), ShouldBeFalse)
}

func TestReopenChannelClient(t *testing.T) {
	a := New(t)
	ctx := getLogger(t, "TestReopenChannelClient")
	c := NewClient(ctx, "guest", "guest", host).(*DefaultClient)
	closed, err := c.connect(false)
	a.So(err, ShouldBeNil)
	defer c.Disconnect()

	publisher := c.NewPublisher("amq.topic")
	err = publisher.Open()
	a.So(err, ShouldBeNil)
	defer publisher.Close()

	subscriber := c.NewSubscriber("amq.topic", "", false, true)
	err = subscriber.Open()
	a.So(err, ShouldBeNil)
	defer subscriber.Close()

	downs := make(chan types.DownlinkMessage, 1)
	err = subscriber.SubscribeDownlink(func(_ Subscriber, appID string, _ string, msg types.DownlinkMessage) {
		a.So(appID, ShouldEqual, "app")
		ctx.Debugf("Got downlink message")
		downs <- msg
	})
	a.So(err, ShouldBeNil)

	test := func() {
		ctx.Debug("Testing publish")
		err := publisher.PublishDownlink(types.DownlinkMessage{
			AppID: "app",
		})
		a.So(err, ShouldBeNil)
		select {
		case <-downs:
		case <-time.After(100 * time.Millisecond):
			panic("Published message didn't come in in time")
		}
		return
	}

	// First attempt should be OK
	test()

	// Make sure that the old channel is closed
	publisher.(*DefaultPublisher).channel.Close()
	subscriber.(*DefaultSubscriber).channel.Close()

	// Simulate a connection close so a new channel should be opened
	closed <- AMQP.ErrClosed

	// Give the reconnect some time
	time.Sleep(100 * time.Millisecond)

	// Second attempt should be OK as well and will only work on a new channel
	test()
	a.So(err, ShouldBeNil)
}
<|endoftext|>


<commit_before>package rules

import (
	"fmt"
	"time"

	"github.com/BurntSushi/toml"
	"github.com/skia-dev/glog"
	"github.com/skia-dev/influxdb/client"
	"go.skia.org/infra/alertserver/go/alerting"
	"golang.org/x/tools/go/exact"
	"golang.org/x/tools/go/types"
)

/*
	Rules for triggering alerts.
*/

// Rule is an object used for triggering Alerts.
type Rule struct {
	Name        string        `json:"name"`
	Query       string        `json:"query"`
	Category    string        `json:"category"`
	Condition   string        `json:"condition"`
	Message     string        `json:"message"`
	Nag         time.Duration `json:"nag"`
	client      queryable
	AutoDismiss int64 `json:"autoDismiss"`
	Actions     []string
}

// Fire causes the Alert to become Active() and not Snoozed(), and causes each
// action to be performed. Active Alerts do not perform new queries.
func (r *Rule) fire(am *alerting.AlertManager, message string) error {
	actions, err := alerting.ParseActions(r.Actions)
	if err != nil {
		return fmt.Errorf("Could not fire alert: %v", err)
	}
	a := alerting.Alert{
		Name:        r.Name,
		Category:    r.Category,
		Message:     message,
		Nag:         int64(r.Nag),
		AutoDismiss: r.AutoDismiss,
		Actions:     actions,
	}
	return am.AddAlert(&a)
}

func (r *Rule) queryExecutionAlert(queryErr error, am *alerting.AlertManager) error {
	actions, err := alerting.ParseActions([]string{"Email(infra-alerts@skia.org)"})
	if err != nil {
		return err
	}
	name := "Failed to execute query"
	msg := fmt.Sprintf("Failed to execute query for rule \"%s\": [ %s ]", r.Name, r.Query)
	glog.Errorf("%s\nFull error:\n%v", msg, queryErr)
	return am.AddAlert(&alerting.Alert{
		Name:     name,
		Category: r.Category, // Should the category be "internal error" or something?
		Message:  msg,
		Nag:      int64(1 * time.Hour),
		Actions:  actions,
	})
}

func (r *Rule) queryEvaluationAlert(queryErr error, am *alerting.AlertManager) error {
	actions, err := alerting.ParseActions([]string{"Email(infra-alerts@skia.org)"})
	if err != nil {
		return err
	}
	name := "Failed to evaluate query"
	msg := fmt.Sprintf("Failed to evaluate query for rule \"%s\": [ %s ]", r.Name, r.Condition)
	glog.Errorf("%s\nFull error:\n%v", msg, queryErr)
	return am.AddAlert(&alerting.Alert{
		Name:     name,
		Category: r.Category, // Should the category be "internal error" or something?
		Message:  msg,
		Nag:      int64(1 * time.Hour),
		Actions:  actions,
	})
}

func (r *Rule) tick(am *alerting.AlertManager) error {
	a := am.ActiveAlert(r.Name)
	if a == 0 {
		d, err := executeQuery(r.client, r.Query)
		if err != nil {
			// We shouldn't fail to execute a query. Trigger an alert.
			return r.queryExecutionAlert(err, am)
		}
		doAlert, err := r.evaluate(d)
		if err != nil {
			return r.queryEvaluationAlert(err, am)
		}
		if doAlert {
			return r.fire(am, r.Message)
		}
	}
	return nil
}

func (r *Rule) evaluate(d float64) (bool, error) {
	pkg := types.NewPackage("evaluateme", "evaluateme")
	v := exact.MakeFloat64(d)
	pkg.Scope().Insert(types.NewConst(0, pkg, "x", types.Typ[types.Float64], v))
	tv, err := types.Eval(r.Condition, pkg, pkg.Scope())
	if err != nil {
		return false, fmt.Errorf("Failed to evaluate condition %q: %s", r.Condition, err)
	}
	if tv.Type.String() != "untyped bool" {
		return false, fmt.Errorf("Rule \"%v\" does not return boolean type.", r.Condition)
	}
	return exact.BoolVal(tv.Value), nil
}

type parsedRule map[string]interface{}

func newRule(r parsedRule, client *client.Client, testing bool, tickInterval time.Duration) (*Rule, error) {
	errString := "Alert rule missing field %q"
	name, ok := r["name"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "name")
	}
	query, ok := r["query"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "query")
	}
	condition, ok := r["condition"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "condition")
	}
	message, ok := r["message"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "message")
	}
	autoDismiss, ok := r["auto-dismiss"].(bool)
	if !ok {
		return nil, fmt.Errorf(errString, "auto-dismiss")
	}
	dismissInterval := int64(0)
	if autoDismiss {
		dismissInterval = int64(10 * tickInterval)
	}
	actionsInterface, ok := r["actions"]
	if !ok {
		return nil, fmt.Errorf(errString, "actions")
	}
	actionsInterfaceList := actionsInterface.([]interface{})
	actionStrings := make([]string, 0, len(actionsInterfaceList))
	for _, iface := range actionsInterfaceList {
		actionStrings = append(actionStrings, iface.(string))
	}
	nagDuration := time.Duration(0)
	nag, ok := r["nag"].(string)
	if ok {
		var err error
		nagDuration, err = time.ParseDuration(nag)
		if err != nil {
			return nil, fmt.Errorf("Invalid nag duration %q: %v", nag, err)
		}
	}
	rule := Rule{
		Name:        name,
		Query:       query,
		Condition:   condition,
		Message:     message,
		Nag:         nagDuration,
		client:      client,
		AutoDismiss: dismissInterval,
		Actions:     actionStrings,
	}
	// Verify that the condition can be evaluated.
	_, err := rule.evaluate(0.0)
	if err != nil {
		return nil, err
	}
	return &rule, nil
}

func parseAlertRules(cfgFile string) ([]parsedRule, error) {
	var cfg struct {
		Rule []parsedRule
	}
	_, err := toml.DecodeFile(cfgFile, &cfg)
	if err != nil {
		return nil, fmt.Errorf("Failed to parse %s: %s", cfgFile, err)
	}
	return cfg.Rule, nil
}

func MakeRules(cfgFile string, dbClient *client.Client, tickInterval time.Duration, am *alerting.AlertManager, testing bool) ([]*Rule, error) {
	parsedRules, err := parseAlertRules(cfgFile)
	if err != nil {
		return nil, err
	}
	rules := []*Rule{}
	for _, r := range parsedRules {
		r, err := newRule(r, dbClient, testing, tickInterval)
		if err != nil {
			return nil, err
		}
		rules = append(rules, r)
	}

	// Start the goroutines.
	for _, r := range rules {
		go func(rule *Rule) {
			if err := rule.tick(am); err != nil {
				glog.Error(err)
			}
			for _ = range time.Tick(tickInterval) {
				if err := rule.tick(am); err != nil {
					glog.Error(err)
				}
			}
		}(r)
	}

	return rules, nil
}

type queryable interface {
	Query(string, ...client.TimePrecision) ([]*client.Series, error)
}

func executeQuery(c queryable, q string) (float64, error) {
	results, err := c.Query(q)
	if err != nil {
		return 0.0, fmt.Errorf("Failed to query InfluxDB with query %q: %s", q, err)
	}
	if len(results) < 1 {
		return 0.0, fmt.Errorf("Query returned no data: %q", q)
	}
	points := results[0].Points
	if len(points) < 1 {
		return 0.0, fmt.Errorf("Query returned no points: %q", q)
	}
	if len(points) > 1 {
		return 0.0, fmt.Errorf("Query returned more than one point: %q", q)
	}
	valueColumn := 0
	for _, label := range results[0].Columns {
		if label == "time" || label == "sequence_number" {
			valueColumn++
		} else {
			break
		}
	}
	if len(results[0].Columns) != valueColumn+1 {
		return 0.0, fmt.Errorf("Query returned an incorrect set of columns: %q %v", q, results[0].Columns)
	}
	if len(results[0].Columns) != len(points[0]) {
		return 0.0, fmt.Errorf("Invalid data from InfluxDB: Point data does not match column spec.")
	}
	return points[0][valueColumn].(float64), nil
}
<commit_msg>Fix Alert auto-dismiss<commit_after>package rules

import (
	"fmt"
	"time"

	"github.com/BurntSushi/toml"
	"github.com/skia-dev/glog"
	"github.com/skia-dev/influxdb/client"
	"go.skia.org/infra/alertserver/go/alerting"
	"golang.org/x/tools/go/exact"
	"golang.org/x/tools/go/types"
)

/*
	Rules for triggering alerts.
*/

// Rule is an object used for triggering Alerts.
type Rule struct {
	Name        string        `json:"name"`
	Query       string        `json:"query"`
	Category    string        `json:"category"`
	Condition   string        `json:"condition"`
	Message     string        `json:"message"`
	Nag         time.Duration `json:"nag"`
	client      queryable
	AutoDismiss int64 `json:"autoDismiss"`
	Actions     []string
}

// Fire causes the Alert to become Active() and not Snoozed(), and causes each
// action to be performed. Active Alerts do not perform new queries.
func (r *Rule) fire(am *alerting.AlertManager, message string) error {
	actions, err := alerting.ParseActions(r.Actions)
	if err != nil {
		return fmt.Errorf("Could not fire alert: %v", err)
	}
	a := alerting.Alert{
		Name:        r.Name,
		Category:    r.Category,
		Message:     message,
		Nag:         int64(r.Nag),
		AutoDismiss: r.AutoDismiss,
		Actions:     actions,
	}
	return am.AddAlert(&a)
}

func (r *Rule) queryExecutionAlert(queryErr error, am *alerting.AlertManager) error {
	actions, err := alerting.ParseActions([]string{"Email(infra-alerts@skia.org)"})
	if err != nil {
		return err
	}
	name := "Failed to execute query"
	msg := fmt.Sprintf("Failed to execute query for rule \"%s\": [ %s ]", r.Name, r.Query)
	glog.Errorf("%s\nFull error:\n%v", msg, queryErr)
	return am.AddAlert(&alerting.Alert{
		Name:     name,
		Category: r.Category, // Should the category be "internal error" or something?
		Message:  msg,
		Nag:      int64(1 * time.Hour),
		Actions:  actions,
	})
}

func (r *Rule) queryEvaluationAlert(queryErr error, am *alerting.AlertManager) error {
	actions, err := alerting.ParseActions([]string{"Email(infra-alerts@skia.org)"})
	if err != nil {
		return err
	}
	name := "Failed to evaluate query"
	msg := fmt.Sprintf("Failed to evaluate query for rule \"%s\": [ %s ]", r.Name, r.Condition)
	glog.Errorf("%s\nFull error:\n%v", msg, queryErr)
	return am.AddAlert(&alerting.Alert{
		Name:     name,
		Category: r.Category, // Should the category be "internal error" or something?
		Message:  msg,
		Nag:      int64(1 * time.Hour),
		Actions:  actions,
	})
}

func (r *Rule) tick(am *alerting.AlertManager) error {
	d, err := executeQuery(r.client, r.Query)
	if err != nil {
		// We shouldn't fail to execute a query. Trigger an alert.
		return r.queryExecutionAlert(err, am)
	}
	doAlert, err := r.evaluate(d)
	if err != nil {
		return r.queryEvaluationAlert(err, am)
	}
	if doAlert {
		return r.fire(am, r.Message)
	}
	return nil
}

func (r *Rule) evaluate(d float64) (bool, error) {
	pkg := types.NewPackage("evaluateme", "evaluateme")
	v := exact.MakeFloat64(d)
	pkg.Scope().Insert(types.NewConst(0, pkg, "x", types.Typ[types.Float64], v))
	tv, err := types.Eval(r.Condition, pkg, pkg.Scope())
	if err != nil {
		return false, fmt.Errorf("Failed to evaluate condition %q: %s", r.Condition, err)
	}
	if tv.Type.String() != "untyped bool" {
		return false, fmt.Errorf("Rule \"%v\" does not return boolean type.", r.Condition)
	}
	return exact.BoolVal(tv.Value), nil
}

type parsedRule map[string]interface{}

func newRule(r parsedRule, client *client.Client, testing bool, tickInterval time.Duration) (*Rule, error) {
	errString := "Alert rule missing field %q"
	name, ok := r["name"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "name")
	}
	query, ok := r["query"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "query")
	}
	condition, ok := r["condition"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "condition")
	}
	message, ok := r["message"].(string)
	if !ok {
		return nil, fmt.Errorf(errString, "message")
	}
	autoDismiss, ok := r["auto-dismiss"].(bool)
	if !ok {
		return nil, fmt.Errorf(errString, "auto-dismiss")
	}
	dismissInterval := int64(0)
	if autoDismiss {
		dismissInterval = int64(10 * tickInterval)
	}
	actionsInterface, ok := r["actions"]
	if !ok {
		return nil, fmt.Errorf(errString, "actions")
	}
	actionsInterfaceList := actionsInterface.([]interface{})
	actionStrings := make([]string, 0, len(actionsInterfaceList))
	for _, iface := range actionsInterfaceList {
		actionStrings = append(actionStrings, iface.(string))
	}
	nagDuration := time.Duration(0)
	nag, ok := r["nag"].(string)
	if ok {
		var err error
		nagDuration, err = time.ParseDuration(nag)
		if err != nil {
			return nil, fmt.Errorf("Invalid nag duration %q: %v", nag, err)
		}
	}
	rule := Rule{
		Name:        name,
		Query:       query,
		Condition:   condition,
		Message:     message,
		Nag:         nagDuration,
		client:      client,
		AutoDismiss: dismissInterval,
		Actions:     actionStrings,
	}
	// Verify that the condition can be evaluated.
	_, err := rule.evaluate(0.0)
	if err != nil {
		return nil, err
	}
	return &rule, nil
}

func parseAlertRules(cfgFile string) ([]parsedRule, error) {
	var cfg struct {
		Rule []parsedRule
	}
	_, err := toml.DecodeFile(cfgFile, &cfg)
	if err != nil {
		return nil, fmt.Errorf("Failed to parse %s: %s", cfgFile, err)
	}
	return cfg.Rule, nil
}

func MakeRules(cfgFile string, dbClient *client.Client, tickInterval time.Duration, am *alerting.AlertManager, testing bool) ([]*Rule, error) {
	parsedRules, err := parseAlertRules(cfgFile)
	if err != nil {
		return nil, err
	}
	rules := []*Rule{}
	for _, r := range parsedRules {
		r, err := newRule(r, dbClient, testing, tickInterval)
		if err != nil {
			return nil, err
		}
		rules = append(rules, r)
	}

	// Start the goroutines.
	for _, r := range rules {
		go func(rule *Rule) {
			if err := rule.tick(am); err != nil {
				glog.Error(err)
			}
			for _ = range time.Tick(tickInterval) {
				if err := rule.tick(am); err != nil {
					glog.Error(err)
				}
			}
		}(r)
	}

	return rules, nil
}

type queryable interface {
	Query(string, ...client.TimePrecision) ([]*client.Series, error)
}

func executeQuery(c queryable, q string) (float64, error) {
	results, err := c.Query(q)
	if err != nil {
		return 0.0, fmt.Errorf("Failed to query InfluxDB with query %q: %s", q, err)
	}
	if len(results) < 1 {
		return 0.0, fmt.Errorf("Query returned no data: %q", q)
	}
	points := results[0].Points
	if len(points) < 1 {
		return 0.0, fmt.Errorf("Query returned no points: %q", q)
	}
	if len(points) > 1 {
		return 0.0, fmt.Errorf("Query returned more than one point: %q", q)
	}
	valueColumn := 0
	for _, label := range results[0].Columns {
		if label == "time" || label == "sequence_number" {
			valueColumn++
		} else {
			break
		}
	}
	if len(results[0].Columns) != valueColumn+1 {
		return 0.0, fmt.Errorf("Query returned an incorrect set of columns: %q %v", q, results[0].Columns)
	}
	if len(results[0].Columns) != len(points[0]) {
		return 0.0, fmt.Errorf("Invalid data from InfluxDB: Point data does not match column spec.")
	}
	return points[0][valueColumn].(float64), nil
}
<|endoftext|>


<commit_before>// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package scanner

import (
	"fmt"
	"regexp"
	"strings"
	"unicode"
	"unicode/utf8"
)

// tokenType identifies the type of lexical tokens.
type tokenType int

// String returns a string representation of the token type.
func (t tokenType) String() string {
	return tokenNames[t]
}

// Token represents a token and the corresponding string.
type Token struct {
	Type   tokenType
	Value  string
	Line   int
	Column int
}

// String returns a string representation of the token.
func (t *Token) String() string {
	if len(t.Value) > 10 {
		return fmt.Sprintf("%s (line: %d, column: %d): %.10q...",
			t.Type, t.Line, t.Column, t.Value)
	}
	return fmt.Sprintf("%s (line: %d, column: %d): %q",
		t.Type, t.Line, t.Column, t.Value)
}

// All tokens -----------------------------------------------------------------

// The complete list of tokens in CSS3.
const (
	// Scanner flags.
	TokenError tokenType = iota
	TokenEOF
	// From now on, only tokens from the CSS specification.
	TokenIdent
	TokenAtKeyword
	TokenString
	TokenHash
	TokenNumber
	TokenPercentage
	TokenDimension
	TokenURI
	TokenUnicodeRange
	TokenCDO
	TokenCDC
	TokenS
	TokenComment
	TokenFunction
	TokenIncludes
	TokenDashMatch
	TokenPrefixMatch
	TokenSuffixMatch
	TokenSubstringMatch
	TokenChar
	TokenBOM
)

// tokenNames maps tokenType's to their names. Used for conversion to string.
var tokenNames = map[tokenType]string{
	TokenError:          "error",
	TokenEOF:            "EOF",
	TokenIdent:          "IDENT",
	TokenAtKeyword:      "ATKEYWORD",
	TokenString:         "STRING",
	TokenHash:           "HASH",
	TokenNumber:         "NUMBER",
	TokenPercentage:     "PERCENTAGE",
	TokenDimension:      "DIMENSION",
	TokenURI:            "URI",
	TokenUnicodeRange:   "UNICODE-RANGE",
	TokenCDO:            "CDO",
	TokenCDC:            "CDC",
	TokenS:              "S",
	TokenComment:        "COMMENT",
	TokenFunction:       "FUNCTION",
	TokenIncludes:       "INCLUDES",
	TokenDashMatch:      "DASHMATCH",
	TokenPrefixMatch:    "PREFIXMATCH",
	TokenSuffixMatch:    "SUFFIXMATCH",
	TokenSubstringMatch: "SUBSTRINGMATCH",
	TokenChar:           "CHAR",
	TokenBOM:            "BOM",
}

// Macros and productions -----------------------------------------------------
// http://www.w3.org/TR/css3-syntax/#tokenization

var macroRegexp = regexp.MustCompile(`\{[a-z]+\}`)

// macros maps macro names to patterns to be expanded.
var macros = map[string]string{
	// must be escaped: `\.+*?()|[]{}^$`
	"ident":      `-?{nmstart}{nmchar}*`,
	"name":       `{nmchar}+`,
	"nmstart":    `[a-zA-Z_]|{nonascii}|{escape}`,
	"nonascii":   "[\u0080-\uD7FF\uE000-\uFFFD\U00010000-\U0010FFFF]",
	"unicode":    `\\[0-9a-fA-F]{1,6}{wc}?`,
	"escape":     "{unicode}|\\[\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\U00010000-\U0010FFFF]",
	"nmchar":     `[a-zA-Z0-9_-]|{nonascii}|{escape}`,
	"num":        `[0-9]+|[0-9]*\.[0-9]+`,
	"string":     `"(?:{stringchar}|')*"|'(?:{stringchar}|")*'`,
	"stringchar": `{urlchar}|[ ]|\\{nl}`,
	"urlchar":    "[\u0009\u0021\u0023-\u0026\u0027-\u007E]|{nonascii}|{escape}",
	"nl":         `[\n\r\f]|\r\n`,
	"w":          `{wc}*`,
	"wc":         `[\t\n\f\r ]`,
}

// productions maps the list of tokens to patterns to be expanded.
var productions = map[tokenType]string{
	TokenIdent:          `{ident}`,
	TokenAtKeyword:      `@{ident}`,
	TokenString:         `{string}`,
	TokenHash:           `#{name}`,
	TokenNumber:         `{num}`,
	TokenPercentage:     `{num}%`,
	TokenDimension:      `{num}{ident}`,
	TokenURI:            `url\({w}(?:{string}|{urlchar}*){w}\)`,
	TokenUnicodeRange:   `U\+[0-9A-F\?]{1,6}(?:-[0-9A-F]{1,6})?`,
	TokenCDO:            `<!--`,
	TokenCDC:            `-->`,
	TokenS:              `{wc}+`,
	TokenComment:        `/\*[^\*]*[\*]+(?:[^/][^\*]*[\*]+)*/`,
	TokenFunction:       `{ident}\(`,
	TokenIncludes:       `~=`,
	TokenDashMatch:      `\|=`,
	TokenPrefixMatch:    `\^=`,
	TokenSuffixMatch:    `\$=`,
	TokenSubstringMatch: `\*=`,
	TokenChar:           `[^"']`,
	TokenBOM:            "\uFEFF",
}

// matchers maps the list of tokens to compiled regular expressions.
//
// The map is filled on init() using the macros and productions defined in
// the CSS specification.
var matchers = map[tokenType]*regexp.Regexp{}

// matchOrder is the order to test regexps when first-char shortcuts
// can't be used.
var matchOrder = []tokenType{
	TokenURI,
	TokenFunction,
	TokenUnicodeRange,
	TokenIdent,
	TokenDimension,
	TokenPercentage,
	TokenNumber,
	TokenCDC,
}

func init() {
	// replace macros and compile regexps for productions.
	replaceMacro := func(s string) string {
		return "(?:" + macros[s[1:len(s)-1]] + ")"
	}
	for t, s := range productions {
		for macroRegexp.MatchString(s) {
			s = macroRegexp.ReplaceAllStringFunc(s, replaceMacro)
		}
		matchers[t] = regexp.MustCompile("^(?:" + s + ")")
	}
}

// Scanner --------------------------------------------------------------------

// New returns a new CSS scanner for the given input.
func New(input string) *Scanner {
	// Normalize newlines.
	input = strings.Replace(input, "\r\n", "\n", -1)
	return &Scanner{
		input: input,
		row:   1,
		col:   1,
	}
}

// Scanner scans an input and emits tokens following the CSS3 specification.
type Scanner struct {
	input string
	pos   int
	row   int
	col   int
	err   *Token
}

// Next returns the next token from the input.
//
// At the end of the input the token type is TokenEOF.
//
// If the input can't be tokenized the token type is TokenError. This occurs
// in case of unclosed quotation marks or comments.
func (s *Scanner) Next() *Token {
	if s.err != nil {
		return s.err
	}
	if s.pos >= len(s.input) {
		s.err = &Token{TokenEOF, "", s.row, s.col}
		return s.err
	}
	if s.pos == 0 {
		// Test BOM only once, at the beginning of the file.
		if strings.HasPrefix(s.input, "\uFEFF") {
			return s.emitSimple(TokenBOM, "\uFEFF")
		}
	}
	// There's a lot we can guess based on the first byte so we'll take a
	// shortcut before testing multiple regexps.
	input := s.input[s.pos:]
	switch input[0] {
	case '\t', '\n', '\f', '\r', ' ':
		// Whitespace.
		return s.emitToken(TokenS, matchers[TokenS].FindString(input))
	case '.':
		// Dot is too common to not have a quick check.
		// We'll test if this is a Char; if it is followed by a number it is a
		// dimension/percentage/number, and this will be matched later.
		if len(input) > 1 && !unicode.IsDigit(rune(input[1])) {
			return s.emitSimple(TokenChar, ".")
		}
	case '#':
		// Another common one: Hash or Char.
		if match := matchers[TokenHash].FindString(input); match != "" {
			return s.emitSimple(TokenHash, match)
		}
		return s.emitSimple(TokenChar, "#")
	case '@':
		// Another common one: AtKeyword or Char.
		if match := matchers[TokenAtKeyword].FindString(input); match != "" {
			return s.emitSimple(TokenAtKeyword, match)
		}
		return s.emitSimple(TokenChar, "@")
	case ':', ',', ';', '%', '&', '+', '=', '>', '(', ')', '[', ']', '{', '}':
		// More common chars.
		return s.emitSimple(TokenChar, string(input[0]))
	case '"', '\'':
		// String or error.
		match := matchers[TokenString].FindString(input)
		if match != "" {
			return s.emitToken(TokenString, match)
		} else {
			s.err = &Token{TokenError, "unclosed quotation mark", s.row, s.col}
			return s.err
		}
	case '/':
		// Comment, error or Char.
		if len(input) > 1 && input[1] == '*' {
			match := matchers[TokenComment].FindString(input)
			if match != "" {
				return s.emitToken(TokenComment, match)
			} else {
				s.err = &Token{TokenError, "unclosed comment", s.row, s.col}
				return s.err
			}
		}
		return s.emitSimple(TokenChar, "/")
	case '~':
		// Includes or Char.
		return s.emitPrefixOrChar(TokenIncludes, "~=")
	case '|':
		// DashMatch or Char.
		return s.emitPrefixOrChar(TokenDashMatch, "|=")
	case '^':
		// PrefixMatch or Char.
		return s.emitPrefixOrChar(TokenPrefixMatch, "^=")
	case '$':
		// SuffixMatch or Char.
		return s.emitPrefixOrChar(TokenSuffixMatch, "$=")
	case '*':
		// SubstringMatch or Char.
		return s.emitPrefixOrChar(TokenSubstringMatch, "*=")
	case '<':
		// CDO or Char.
		return s.emitPrefixOrChar(TokenCDO, "<!--")
	}
	// Test all regexps, in order.
	for _, token := range matchOrder {
		if match := matchers[token].FindString(input); match != "" {
			return s.emitToken(token, match)
		}
	}
	// We already handled unclosed quotation marks and comments,
	// so this can only be a Char.
	r, width := utf8.DecodeRuneInString(input)
	token := &Token{TokenChar, string(r), s.row, s.col}
	s.col += width
	s.pos += width
	return token
}

// updatePosition updates input coordinates based on the consumed text.
func (s *Scanner) updatePosition(text string) {
	width := utf8.RuneCountInString(text)
	lines := strings.Count(text, "\n")
	s.row += lines
	if lines == 0 {
		s.col += width
	} else {
		s.col = utf8.RuneCountInString(text[strings.LastIndex(text, "\n"):])
	}
	s.pos += width
}

// emitToken returns a Token for the string v and updates the scanner position.
func (s *Scanner) emitToken(t tokenType, v string) *Token {
	token := &Token{t, v, s.row, s.col}
	s.updatePosition(v)
	return token
}

// emitSimple is a shortcut to emit a token with simplified position updates.
//
// The string is known to have only ASCII characters and to not have a newline.
func (s *Scanner) emitSimple(t tokenType, v string) *Token {
	token := &Token{t, v, s.row, s.col}
	s.col += len(v)
	s.pos += len(v)
	return token
}

// emitPrefixOrChar returns a Token for type t if the current position
// matches the given prefix. Otherwise it returns a Char token using the
// first character from the prefix.
//
// The prefix is known to have only ASCII characters and to not have a newline.
func (s *Scanner) emitPrefixOrChar(t tokenType, prefix string) *Token {
	if strings.HasPrefix(s.input[s.pos:], prefix) {
		return s.emitSimple(t, prefix)
	}
	return s.emitSimple(TokenChar, string(prefix[0]))
}
<commit_msg>css/scanner: comment out unused regexps.<commit_after>// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package scanner

import (
	"fmt"
	"regexp"
	"strings"
	"unicode"
	"unicode/utf8"
)

// tokenType identifies the type of lexical tokens.
type tokenType int

// String returns a string representation of the token type.
func (t tokenType) String() string {
	return tokenNames[t]
}

// Token represents a token and the corresponding string.
type Token struct {
	Type   tokenType
	Value  string
	Line   int
	Column int
}

// String returns a string representation of the token.
func (t *Token) String() string {
	if len(t.Value) > 10 {
		return fmt.Sprintf("%s (line: %d, column: %d): %.10q...",
			t.Type, t.Line, t.Column, t.Value)
	}
	return fmt.Sprintf("%s (line: %d, column: %d): %q",
		t.Type, t.Line, t.Column, t.Value)
}

// All tokens -----------------------------------------------------------------

// The complete list of tokens in CSS3.
const (
	// Scanner flags.
	TokenError tokenType = iota
	TokenEOF
	// From now on, only tokens from the CSS specification.
	TokenIdent
	TokenAtKeyword
	TokenString
	TokenHash
	TokenNumber
	TokenPercentage
	TokenDimension
	TokenURI
	TokenUnicodeRange
	TokenCDO
	TokenCDC
	TokenS
	TokenComment
	TokenFunction
	TokenIncludes
	TokenDashMatch
	TokenPrefixMatch
	TokenSuffixMatch
	TokenSubstringMatch
	TokenChar
	TokenBOM
)

// tokenNames maps tokenType's to their names. Used for conversion to string.
var tokenNames = map[tokenType]string{
	TokenError:          "error",
	TokenEOF:            "EOF",
	TokenIdent:          "IDENT",
	TokenAtKeyword:      "ATKEYWORD",
	TokenString:         "STRING",
	TokenHash:           "HASH",
	TokenNumber:         "NUMBER",
	TokenPercentage:     "PERCENTAGE",
	TokenDimension:      "DIMENSION",
	TokenURI:            "URI",
	TokenUnicodeRange:   "UNICODE-RANGE",
	TokenCDO:            "CDO",
	TokenCDC:            "CDC",
	TokenS:              "S",
	TokenComment:        "COMMENT",
	TokenFunction:       "FUNCTION",
	TokenIncludes:       "INCLUDES",
	TokenDashMatch:      "DASHMATCH",
	TokenPrefixMatch:    "PREFIXMATCH",
	TokenSuffixMatch:    "SUFFIXMATCH",
	TokenSubstringMatch: "SUBSTRINGMATCH",
	TokenChar:           "CHAR",
	TokenBOM:            "BOM",
}

// Macros and productions -----------------------------------------------------
// http://www.w3.org/TR/css3-syntax/#tokenization

var macroRegexp = regexp.MustCompile(`\{[a-z]+\}`)

// macros maps macro names to patterns to be expanded.
var macros = map[string]string{
	// must be escaped: `\.+*?()|[]{}^$`
	"ident":      `-?{nmstart}{nmchar}*`,
	"name":       `{nmchar}+`,
	"nmstart":    `[a-zA-Z_]|{nonascii}|{escape}`,
	"nonascii":   "[\u0080-\uD7FF\uE000-\uFFFD\U00010000-\U0010FFFF]",
	"unicode":    `\\[0-9a-fA-F]{1,6}{wc}?`,
	"escape":     "{unicode}|\\[\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\U00010000-\U0010FFFF]",
	"nmchar":     `[a-zA-Z0-9_-]|{nonascii}|{escape}`,
	"num":        `[0-9]+|[0-9]*\.[0-9]+`,
	"string":     `"(?:{stringchar}|')*"|'(?:{stringchar}|")*'`,
	"stringchar": `{urlchar}|[ ]|\\{nl}`,
	"urlchar":    "[\u0009\u0021\u0023-\u0026\u0027-\u007E]|{nonascii}|{escape}",
	"nl":         `[\n\r\f]|\r\n`,
	"w":          `{wc}*`,
	"wc":         `[\t\n\f\r ]`,
}

// productions maps the list of tokens to patterns to be expanded.
var productions = map[tokenType]string{
	// Unused regexps (matched using other methods) are commented out.
	TokenIdent:          `{ident}`,
	TokenAtKeyword:      `@{ident}`,
	TokenString:         `{string}`,
	TokenHash:           `#{name}`,
	TokenNumber:         `{num}`,
	TokenPercentage:     `{num}%`,
	TokenDimension:      `{num}{ident}`,
	TokenURI:            `url\({w}(?:{string}|{urlchar}*){w}\)`,
	TokenUnicodeRange:   `U\+[0-9A-F\?]{1,6}(?:-[0-9A-F]{1,6})?`,
	//TokenCDO:            `<!--`,
	TokenCDC:            `-->`,
	TokenS:              `{wc}+`,
	TokenComment:        `/\*[^\*]*[\*]+(?:[^/][^\*]*[\*]+)*/`,
	TokenFunction:       `{ident}\(`,
	//TokenIncludes:       `~=`,
	//TokenDashMatch:      `\|=`,
	//TokenPrefixMatch:    `\^=`,
	//TokenSuffixMatch:    `\$=`,
	//TokenSubstringMatch: `\*=`,
	//TokenChar:           `[^"']`,
	//TokenBOM:            "\uFEFF",
}

// matchers maps the list of tokens to compiled regular expressions.
//
// The map is filled on init() using the macros and productions defined in
// the CSS specification.
var matchers = map[tokenType]*regexp.Regexp{}

// matchOrder is the order to test regexps when first-char shortcuts
// can't be used.
var matchOrder = []tokenType{
	TokenURI,
	TokenFunction,
	TokenUnicodeRange,
	TokenIdent,
	TokenDimension,
	TokenPercentage,
	TokenNumber,
	TokenCDC,
}

func init() {
	// replace macros and compile regexps for productions.
	replaceMacro := func(s string) string {
		return "(?:" + macros[s[1:len(s)-1]] + ")"
	}
	for t, s := range productions {
		for macroRegexp.MatchString(s) {
			s = macroRegexp.ReplaceAllStringFunc(s, replaceMacro)
		}
		matchers[t] = regexp.MustCompile("^(?:" + s + ")")
	}
}

// Scanner --------------------------------------------------------------------

// New returns a new CSS scanner for the given input.
func New(input string) *Scanner {
	// Normalize newlines.
	input = strings.Replace(input, "\r\n", "\n", -1)
	return &Scanner{
		input: input,
		row:   1,
		col:   1,
	}
}

// Scanner scans an input and emits tokens following the CSS3 specification.
type Scanner struct {
	input string
	pos   int
	row   int
	col   int
	err   *Token
}

// Next returns the next token from the input.
//
// At the end of the input the token type is TokenEOF.
//
// If the input can't be tokenized the token type is TokenError. This occurs
// in case of unclosed quotation marks or comments.
func (s *Scanner) Next() *Token {
	if s.err != nil {
		return s.err
	}
	if s.pos >= len(s.input) {
		s.err = &Token{TokenEOF, "", s.row, s.col}
		return s.err
	}
	if s.pos == 0 {
		// Test BOM only once, at the beginning of the file.
		if strings.HasPrefix(s.input, "\uFEFF") {
			return s.emitSimple(TokenBOM, "\uFEFF")
		}
	}
	// There's a lot we can guess based on the first byte so we'll take a
	// shortcut before testing multiple regexps.
	input := s.input[s.pos:]
	switch input[0] {
	case '\t', '\n', '\f', '\r', ' ':
		// Whitespace.
		return s.emitToken(TokenS, matchers[TokenS].FindString(input))
	case '.':
		// Dot is too common to not have a quick check.
		// We'll test if this is a Char; if it is followed by a number it is a
		// dimension/percentage/number, and this will be matched later.
		if len(input) > 1 && !unicode.IsDigit(rune(input[1])) {
			return s.emitSimple(TokenChar, ".")
		}
	case '#':
		// Another common one: Hash or Char.
		if match := matchers[TokenHash].FindString(input); match != "" {
			return s.emitSimple(TokenHash, match)
		}
		return s.emitSimple(TokenChar, "#")
	case '@':
		// Another common one: AtKeyword or Char.
		if match := matchers[TokenAtKeyword].FindString(input); match != "" {
			return s.emitSimple(TokenAtKeyword, match)
		}
		return s.emitSimple(TokenChar, "@")
	case ':', ',', ';', '%', '&', '+', '=', '>', '(', ')', '[', ']', '{', '}':
		// More common chars.
		return s.emitSimple(TokenChar, string(input[0]))
	case '"', '\'':
		// String or error.
		match := matchers[TokenString].FindString(input)
		if match != "" {
			return s.emitToken(TokenString, match)
		} else {
			s.err = &Token{TokenError, "unclosed quotation mark", s.row, s.col}
			return s.err
		}
	case '/':
		// Comment, error or Char.
		if len(input) > 1 && input[1] == '*' {
			match := matchers[TokenComment].FindString(input)
			if match != "" {
				return s.emitToken(TokenComment, match)
			} else {
				s.err = &Token{TokenError, "unclosed comment", s.row, s.col}
				return s.err
			}
		}
		return s.emitSimple(TokenChar, "/")
	case '~':
		// Includes or Char.
		return s.emitPrefixOrChar(TokenIncludes, "~=")
	case '|':
		// DashMatch or Char.
		return s.emitPrefixOrChar(TokenDashMatch, "|=")
	case '^':
		// PrefixMatch or Char.
		return s.emitPrefixOrChar(TokenPrefixMatch, "^=")
	case '$':
		// SuffixMatch or Char.
		return s.emitPrefixOrChar(TokenSuffixMatch, "$=")
	case '*':
		// SubstringMatch or Char.
		return s.emitPrefixOrChar(TokenSubstringMatch, "*=")
	case '<':
		// CDO or Char.
		return s.emitPrefixOrChar(TokenCDO, "<!--")
	}
	// Test all regexps, in order.
	for _, token := range matchOrder {
		if match := matchers[token].FindString(input); match != "" {
			return s.emitToken(token, match)
		}
	}
	// We already handled unclosed quotation marks and comments,
	// so this can only be a Char.
	r, width := utf8.DecodeRuneInString(input)
	token := &Token{TokenChar, string(r), s.row, s.col}
	s.col += width
	s.pos += width
	return token
}

// updatePosition updates input coordinates based on the consumed text.
func (s *Scanner) updatePosition(text string) {
	width := utf8.RuneCountInString(text)
	lines := strings.Count(text, "\n")
	s.row += lines
	if lines == 0 {
		s.col += width
	} else {
		s.col = utf8.RuneCountInString(text[strings.LastIndex(text, "\n"):])
	}
	s.pos += width
}

// emitToken returns a Token for the string v and updates the scanner position.
func (s *Scanner) emitToken(t tokenType, v string) *Token {
	token := &Token{t, v, s.row, s.col}
	s.updatePosition(v)
	return token
}

// emitSimple is a shortcut to emit a token with simplified position updates.
//
// The string is known to have only ASCII characters and to not have a newline.
func (s *Scanner) emitSimple(t tokenType, v string) *Token {
	token := &Token{t, v, s.row, s.col}
	s.col += len(v)
	s.pos += len(v)
	return token
}

// emitPrefixOrChar returns a Token for type t if the current position
// matches the given prefix. Otherwise it returns a Char token using the
// first character from the prefix.
//
// The prefix is known to have only ASCII characters and to not have a newline.
func (s *Scanner) emitPrefixOrChar(t tokenType, prefix string) *Token {
	if strings.HasPrefix(s.input[s.pos:], prefix) {
		return s.emitSimple(t, prefix)
	}
	return s.emitSimple(TokenChar, string(prefix[0]))
}
<|endoftext|>