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

package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestPortableAPIDemos(t *testing.T) {
	dir, cleanup, err := newTestModule()
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()

	pctx := &processContext{
		workdir: dir,
		env:     os.Environ(),
		stdout:  ioutil.Discard,
		stderr:  ioutil.Discard,
	}
	ctx := context.Background()

	if err := run(ctx, pctx, []string{"init", "-m", "test", "--allow-existing-dir", dir}, new(bool)); err != nil {
		t.Fatalf("run init error: %+v", err)
	}

	// Call the main package run function as if 'add-api' were being called
	// from the command line for each of the portable APIs.
	for _, api := range portableAPIs {
		if err := run(ctx, pctx, []string{"add-api", api.name}, new(bool)); err != nil {
			t.Fatalf("run add-api error: %+v", err)
		}
	}

	// Build the binary.
	exePath := filepath.Join(dir, "add-api-test")
	if err := buildForServe(ctx, pctx, dir, exePath); err != nil {
		t.Fatal("buildForServe(...):", err)
	}

	// Update the environment with to use local implementations for each
	// portable API.
	pctx.env = pctx.overrideEnv(
		"BLOB_BUCKET_URL=mem://",
	)

	// Run the program, listening on a free port.
	logger := log.New(pctx.stderr, "gocdk demo server under test: ", log.Ldate|log.Ltime)
	alloc := &serverAlloc{exePath: exePath, port: findFreePort()}
	cmd, err := alloc.start(ctx, pctx, logger, pctx.workdir)
	if err != nil {
		t.Fatalf("failed to start server: %v", err)
	}
	defer endServerProcess(cmd)

	tests := []struct {
		api           string
		description   string
		urlPath       string
		op            string
		urlValues     url.Values // only used if op=POST
		stringsToFind []string
	}{
		{
			api:         "blob",
			description: "base no slash",
			urlPath:     "/demo/blob",
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"This page demonstrates the use",
				"https://godoc.org/gocloud.dev/blob",
				`<a href="./list">List</a>`,
				`<a href="./view">View</a>`,
				`<a href="./write">Write</a>`,
			},
		},
		{
			api:         "blob",
			description: "base with slash",
			urlPath:     "/demo/blob/",
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"This page demonstrates the use",
				"https://godoc.org/gocloud.dev/blob",
				`<a href="./list">List</a>`,
				`<a href="./view">View</a>`,
				`<a href="./write">Write</a>`,
			},
		},
		{
			api:         "blob",
			description: "list: empty",
			urlPath:     "/demo/blob/list",
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"no blobs in bucket",
			},
		},
		{
			api:         "blob",
			description: "view: empty",
			urlPath:     "/demo/blob/list",
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"no blobs in bucket",
			},
		},
		{
			api:         "blob",
			description: "write: empty form",
			urlPath:     "/demo/blob/write",
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				`<input type="submit" value="Write It!">`, // form is shown
			},
		},
		{
			api:         "blob",
			description: "write: missing key",
			urlPath:     "/demo/blob/write",
			op:          "POST",
			urlValues:   map[string][]string{"contents": {"foo"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"enter a non-empty key to write to",
				"foo", // previous entry for contents field is carried over
				`<input type="submit" value="Write It!">`, // form is shown
			},
		},
		{
			api:         "blob",
			description: "write: missing contents",
			urlPath:     "/demo/blob/write",
			op:          "POST",
			urlValues:   map[string][]string{"key": {"key1"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"enter some content to write",
				"key1", // previous entry for key field is carried over
				`<input type="submit" value="Write It!">`, // form is shown
			},
		},
		{
			api:         "blob",
			description: "write: top level key",
			urlPath:     "/demo/blob/write",
			op:          "POST",
			urlValues:   map[string][]string{"key": {"key1"}, "contents": {"key1 contents"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"Wrote it!",
			},
		},
		{
			api:         "blob",
			description: "write: subdirectory key",
			urlPath:     "/demo/blob/write",
			op:          "POST",
			urlValues:   map[string][]string{"key": {"subdir/key2"}, "contents": {"key2 contents"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"Wrote it!",
			},
		},
		{
			api:         "blob",
			description: "list: no longer empty",
			urlPath:     "/demo/blob/list",
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				`<a href="./view?key=key1">key1</a>`,
				`<a href="./list?prefix=subdir%2f">subdir/</a>`,
			},
		},
		/*
			TODO(rvangent): Enable listing of a subdir; broken right now because
			serverAlloc.url doesn't handle query parameters correctly.
			{
				api:          "blob",
				description: "list: subdir",
				urlPath:    "/demo/blob/list?prefix=subdir%2f",
				op:          "GET",
				stringsToFind: []string{
					"<title>gocloud.dev/blob demo</title>",
					`<a href="./view?key=subdir%2fkey2">key2</a>`,
				},
			},
		*/
		// TODO(rvangent): Add tests for the view page.
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("%s (%s, %s)", test.api, test.description, test.urlPath), func(t *testing.T) {
			u := alloc.url(test.urlPath).String()
			var resp *http.Response
			var err error
			switch test.op {
			case "GET":
				resp, err = http.DefaultClient.Get(u)
			case "POST":
				resp, err = http.DefaultClient.PostForm(u, test.urlValues)
			default:
				t.Fatalf("invalid test.op: %q", test.op)
			}
			if err != nil {
				t.Fatalf("HTTP %q request failed: %v", test.op, err)
			}
			defer resp.Body.Close()
			if resp.StatusCode != http.StatusOK {
				t.Fatalf("HTTP request returned status code %v, want %v", resp.StatusCode, http.StatusOK)
			}
			bodyBytes, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				t.Errorf("failed to read HTTP response body: %v", err)
			}
			body := string(bodyBytes)
			logBody := false // only log the body on failure, and only once per test
			for _, s := range test.stringsToFind {
				if !strings.Contains(body, s) {
					t.Errorf("didn't find %q in HTTP response body", s)
					logBody = true
				}
			}
			if logBody {
				t.Error("Full HTTP response body:\n\n", body)
			}
		})
	}
}

func findFreePort() int {
	l, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		log.Fatalf("failed to Listen to localhost:0; no free ports?: %v", err)
	}
	defer l.Close()
	return l.Addr().(*net.TCPAddr).Port
}
<commit_msg>gocdk: add more tests for the blob demo (#2043)<commit_after>// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestPortableAPIDemos(t *testing.T) {
	dir, cleanup, err := newTestModule()
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()

	pctx := &processContext{
		workdir: dir,
		env:     os.Environ(),
		stdout:  ioutil.Discard,
		stderr:  ioutil.Discard,
	}
	ctx := context.Background()

	if err := run(ctx, pctx, []string{"init", "-m", "test", "--allow-existing-dir", dir}, new(bool)); err != nil {
		t.Fatalf("run init error: %+v", err)
	}

	// Call the main package run function as if 'add-api' were being called
	// from the command line for each of the portable APIs.
	for _, api := range portableAPIs {
		if err := run(ctx, pctx, []string{"add-api", api.name}, new(bool)); err != nil {
			t.Fatalf("run add-api error: %+v", err)
		}
	}

	// Build the binary.
	exePath := filepath.Join(dir, "add-api-test")
	if err := buildForServe(ctx, pctx, dir, exePath); err != nil {
		t.Fatal("buildForServe(...):", err)
	}

	// Update the environment with to use local implementations for each
	// portable API.
	pctx.env = pctx.overrideEnv(
		"BLOB_BUCKET_URL=mem://",
	)

	// Run the program, listening on a free port.
	logger := log.New(pctx.stderr, "gocdk demo server under test: ", log.Ldate|log.Ltime)
	alloc := &serverAlloc{exePath: exePath, port: findFreePort()}
	cmd, err := alloc.start(ctx, pctx, logger, pctx.workdir)
	if err != nil {
		t.Fatalf("failed to start server: %v", err)
	}
	defer endServerProcess(cmd)

	tests := []struct {
		api           string
		description   string
		urlPaths      []string
		op            string
		urlQuery      string
		urlValues     url.Values // only used if op=POST
		stringsToFind []string
	}{
		{
			api:         "blob",
			description: "base",
			urlPaths:    []string{"/demo/blob", "/demo/blob/"},
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"This page demonstrates the use",
				"https://godoc.org/gocloud.dev/blob",
				`<a href="./list">List</a>`,
				`<a href="./view">View</a>`,
				`<a href="./write">Write</a>`,
			},
		},
		{
			api:         "blob",
			description: "list+view: empty bucket",
			urlPaths:    []string{"/demo/blob/list", "/demo/blob/view"},
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"no blobs in bucket",
			},
		},
		{
			api:         "blob",
			description: "write: empty form",
			urlPaths:    []string{"/demo/blob/write"},
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				`<input type="submit" value="Write It!">`, // form is shown
			},
		},
		{
			api:         "blob",
			description: "write: missing key",
			urlPaths:    []string{"/demo/blob/write"},
			op:          "POST",
			urlValues:   map[string][]string{"contents": {"foo"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"enter a non-empty key to write to",
				"foo", // previous entry for contents field is carried over
				`<input type="submit" value="Write It!">`, // form is shown
			},
		},
		{
			api:         "blob",
			description: "write: missing contents",
			urlPaths:    []string{"/demo/blob/write"},
			op:          "POST",
			urlValues:   map[string][]string{"key": {"key1"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"enter some content to write",
				"key1", // previous entry for key field is carried over
				`<input type="submit" value="Write It!">`, // form is shown
			},
		},
		{
			api:         "blob",
			description: "write: top level key",
			urlPaths:    []string{"/demo/blob/write"},
			op:          "POST",
			urlValues:   map[string][]string{"key": {"key1"}, "contents": {"key1 contents"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"Wrote it!",
			},
		},
		{
			api:         "blob",
			description: "write: subdirectory key",
			urlPaths:    []string{"/demo/blob/write"},
			op:          "POST",
			urlValues:   map[string][]string{"key": {"subdir/key2"}, "contents": {"key2 contents"}},
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"Wrote it!",
			},
		},
		{
			api:         "blob",
			description: "list: no longer empty",
			urlPaths:    []string{"/demo/blob/list"},
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				`<a href="./view?key=key1">key1</a>`,
				`<a href="./list?prefix=subdir%2f">subdir/</a>`,
			},
		},
		{
			api:         "blob",
			description: "list: subdir",
			urlPaths:    []string{"/demo/blob/list"},
			urlQuery:    "prefix=subdir%2f",
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				`<a href="./view?key=subdir%2fkey2">subdir/key2</a>`,
			},
		},
		{
			api:         "blob",
			description: "view: none selected",
			urlPaths:    []string{"/demo/blob/view"},
			op:          "GET",
			stringsToFind: []string{
				"<title>gocloud.dev/blob demo</title>",
				"Choose a blob",
				`<option value="key1">key1</option>`,
				`<option value="subdir/key2">subdir/key2</option>`,
			},
		},
		{
			api:         "blob",
			description: "view: key1 selected",
			urlPaths:    []string{"/demo/blob/view"},
			urlQuery:    "key=key1",
			op:          "GET",
			stringsToFind: []string{
				"key1 contents",
			},
		},
		{
			api:         "blob",
			description: "view: key2 selected",
			urlPaths:    []string{"/demo/blob/view"},
			urlQuery:    "key=subdir%2fkey2",
			op:          "GET",
			stringsToFind: []string{
				"key2 contents",
			},
		},
	}

	for _, test := range tests {
		for _, urlPath := range test.urlPaths {
			queryURL := alloc.url(urlPath)
			queryURL.RawQuery = test.urlQuery
			u := queryURL.String()
			t.Run(fmt.Sprintf("%s %s: %s", test.api, test.description, u), func(t *testing.T) {
				var resp *http.Response
				var err error
				switch test.op {
				case "GET":
					resp, err = http.DefaultClient.Get(u)
				case "POST":
					resp, err = http.DefaultClient.PostForm(u, test.urlValues)
				default:
					t.Fatalf("invalid test.op: %q", test.op)
				}
				if err != nil {
					t.Fatalf("HTTP %q request failed: %v", test.op, err)
				}
				defer resp.Body.Close()
				if resp.StatusCode != http.StatusOK {
					t.Fatalf("HTTP request returned status code %v, want %v", resp.StatusCode, http.StatusOK)
				}
				bodyBytes, err := ioutil.ReadAll(resp.Body)
				if err != nil {
					t.Errorf("failed to read HTTP response body: %v", err)
				}
				body := string(bodyBytes)
				logBody := false // only log the body on failure, and only once per test
				for _, s := range test.stringsToFind {
					if !strings.Contains(body, s) {
						t.Errorf("didn't find %q in HTTP response body", s)
						logBody = true
					}
				}
				if logBody {
					t.Error("Full HTTP response body:\n\n", body)
				}
			})
		}
	}
}

func findFreePort() int {
	l, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		log.Fatalf("failed to Listen to localhost:0; no free ports?: %v", err)
	}
	defer l.Close()
	return l.Addr().(*net.TCPAddr).Port
}
<|endoftext|>


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

package wrappers

import (
	"context"
	"errors"
	"fmt"
	"log"
	"syscall"
	"time"

	"github.com/googlecloudplatform/gcsfuse/internal/logger"
	"github.com/googlecloudplatform/gcsfuse/internal/monitor/tags"
	"github.com/jacobsa/fuse/fuseops"
	"github.com/jacobsa/fuse/fuseutil"
	"go.opencensus.io/plugin/ochttp"
	"go.opencensus.io/stats"
	"go.opencensus.io/stats/view"
	"go.opencensus.io/tag"
)

var errorLogger *log.Logger

var (
	opCount      = stats.Int64("fs/opCount", "The number of ops processed by the file system.", stats.UnitDimensionless)
	opLatency    = stats.Float64("fs/opLatency", "The latency of a file system operation.", stats.UnitMilliseconds)
	opErrorCount = stats.Int64("fs/opErrorCount", "The number of errors generated by file system operation.", stats.UnitDimensionless)
)

// Initialize the metrics.
func init() {

	errorLogger = logger.NewError("")

	// Register the view
	if err := view.Register(
		&view.View{
			Name:        "fs/opCount",
			Measure:     opCount,
			Description: "The cumulative number of ops processed by the file system.",
			Aggregation: view.Sum(),
			TagKeys:     []tag.Key{tags.FSOp, tags.FSError},
		},
		&view.View{
			Name:        "fs/opErrorCount",
			Measure:     opErrorCount,
			Description: "The cumulative number of errors generated by file system operations",
			Aggregation: view.Sum(),
			TagKeys:     []tag.Key{tags.FSOp},
		},
		&view.View{
			Name:        "fs/opLatency",
			Measure:     opLatency,
			Description: "The cumulative distribution of file system operation latencies",
			Aggregation: ochttp.DefaultLatencyDistribution,
			TagKeys:     []tag.Key{tags.FSOp},
		}); err != nil {
		fmt.Printf("Failed to register metrics for the file system: %v\n", err)
	}
}

// fsErrStr maps an error to a error string. Uncommon errors are aggregated to
// reduce the cardinality of the fs error to save the monitoring cost.
func fsErrStr(err error) string {
	if err == nil {
		return ""
	}
	var errno syscall.Errno
	if errors.As(err, &errno) {
		return errno.Error()
	}
	return DefaultFSError.Error()
}

// recordOp records file system operation count, operation error count and the operation latency
func recordOp(ctx context.Context, method string, start time.Time, fsErr error) {

	// Recording opCount
	if err := stats.RecordWithTags(
		ctx,
		[]tag.Mutator{
			tag.Upsert(tags.FSOp, method),
			tag.Upsert(tags.FSError, fsErrStr(fsErr)),
		},
		opCount.M(1),
	); err != nil {
		// Error in recording opCount
		fmt.Printf("Cannot record file system op: %v", err)
	}

	// Recording opErrorCount
	if fsErr != nil {
		if err := stats.RecordWithTags(
			ctx,
			[]tag.Mutator{
				tag.Upsert(tags.FSOp, method),
			},
			opErrorCount.M(1),
		); err != nil {
			// Error in recording opErrorCount
			errorLogger.Printf("Cannot record error count of the file system failed operations: %v", err)
		}
	}

	// Recording opLatency
	latencyMs := float64(time.Since(start).Microseconds()) / 1000.0
	if err := stats.RecordWithTags(
		ctx,
		[]tag.Mutator{
			tag.Upsert(tags.FSOp, method),
		},
		opLatency.M(latencyMs),
	); err != nil {
		// Error in opLatency
		errorLogger.Printf("Cannot record file system operation latency: %v", err)
	}
}

// WithMonitoring takes a FileSystem, returns a FileSystem with monitoring
// on the counts of requests per API.
func WithMonitoring(fs fuseutil.FileSystem) fuseutil.FileSystem {
	return &monitoring{
		wrapped: fs,
	}
}

type monitoring struct {
	wrapped fuseutil.FileSystem
}

func (fs *monitoring) Destroy() {
	fs.wrapped.Destroy()
}

func (fs *monitoring) StatFS(
	ctx context.Context,
	op *fuseops.StatFSOp) error {
	startTime := time.Now()
	err := fs.wrapped.StatFS(ctx, op)
	recordOp(ctx, "StatFS", startTime, err)
	return err
}

func (fs *monitoring) LookUpInode(
	ctx context.Context,
	op *fuseops.LookUpInodeOp) error {
	startTime := time.Now()
	err := fs.wrapped.LookUpInode(ctx, op)
	recordOp(ctx, "LookUpInode", startTime, err)
	return err
}

func (fs *monitoring) GetInodeAttributes(
	ctx context.Context,
	op *fuseops.GetInodeAttributesOp) error {
	startTime := time.Now()
	err := fs.wrapped.GetInodeAttributes(ctx, op)
	recordOp(ctx, "GetInodeAttributes", startTime, err)
	return err
}

func (fs *monitoring) SetInodeAttributes(
	ctx context.Context,
	op *fuseops.SetInodeAttributesOp) error {
	startTime := time.Now()
	err := fs.wrapped.SetInodeAttributes(ctx, op)
	recordOp(ctx, "SetInodeAttributes", startTime, err)
	return err
}

func (fs *monitoring) ForgetInode(
	ctx context.Context,
	op *fuseops.ForgetInodeOp) error {
	startTime := time.Now()
	err := fs.wrapped.ForgetInode(ctx, op)
	recordOp(ctx, "ForgetInode", startTime, err)
	return err
}

func (fs *monitoring) MkDir(
	ctx context.Context,
	op *fuseops.MkDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.MkDir(ctx, op)
	recordOp(ctx, "MkDir", startTime, err)
	return err
}

func (fs *monitoring) MkNode(
	ctx context.Context,
	op *fuseops.MkNodeOp) error {
	startTime := time.Now()
	err := fs.wrapped.MkNode(ctx, op)
	recordOp(ctx, "MkNode", startTime, err)
	return err
}

func (fs *monitoring) CreateFile(
	ctx context.Context,
	op *fuseops.CreateFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.CreateFile(ctx, op)
	recordOp(ctx, "CreateFile", startTime, err)
	return err
}

func (fs *monitoring) CreateLink(
	ctx context.Context,
	op *fuseops.CreateLinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.CreateLink(ctx, op)
	recordOp(ctx, "CreateLink", startTime, err)
	return err
}

func (fs *monitoring) CreateSymlink(
	ctx context.Context,
	op *fuseops.CreateSymlinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.CreateSymlink(ctx, op)
	recordOp(ctx, "CreateSymlink", startTime, err)
	return err
}

func (fs *monitoring) Rename(
	ctx context.Context,
	op *fuseops.RenameOp) error {
	startTime := time.Now()
	err := fs.wrapped.Rename(ctx, op)
	recordOp(ctx, "Rename", startTime, err)
	return err
}

func (fs *monitoring) RmDir(
	ctx context.Context,
	op *fuseops.RmDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.RmDir(ctx, op)
	recordOp(ctx, "RmDir", startTime, err)
	return err
}

func (fs *monitoring) Unlink(
	ctx context.Context,
	op *fuseops.UnlinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.Unlink(ctx, op)
	recordOp(ctx, "Unlink", startTime, err)
	return err
}

func (fs *monitoring) OpenDir(
	ctx context.Context,
	op *fuseops.OpenDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.OpenDir(ctx, op)
	recordOp(ctx, "OpenDir", startTime, err)
	return err
}

func (fs *monitoring) ReadDir(
	ctx context.Context,
	op *fuseops.ReadDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReadDir(ctx, op)
	recordOp(ctx, "ReadDir", startTime, err)
	return err
}

func (fs *monitoring) ReleaseDirHandle(
	ctx context.Context,
	op *fuseops.ReleaseDirHandleOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReleaseDirHandle(ctx, op)
	recordOp(ctx, "ReleaseDirHandle", startTime, err)
	return err
}

func (fs *monitoring) OpenFile(
	ctx context.Context,
	op *fuseops.OpenFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.OpenFile(ctx, op)
	recordOp(ctx, "OpenFile", startTime, err)
	return err
}

func (fs *monitoring) ReadFile(
	ctx context.Context,
	op *fuseops.ReadFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReadFile(ctx, op)
	recordOp(ctx, "ReadFile", startTime, err)
	return err
}

func (fs *monitoring) WriteFile(
	ctx context.Context,
	op *fuseops.WriteFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.WriteFile(ctx, op)
	recordOp(ctx, "WriteFile", startTime, err)
	return err
}

func (fs *monitoring) SyncFile(
	ctx context.Context,
	op *fuseops.SyncFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.SyncFile(ctx, op)
	recordOp(ctx, "SyncFile", startTime, err)
	return err
}

func (fs *monitoring) FlushFile(
	ctx context.Context,
	op *fuseops.FlushFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.FlushFile(ctx, op)
	recordOp(ctx, "FlushFile", startTime, err)
	return err
}

func (fs *monitoring) ReleaseFileHandle(
	ctx context.Context,
	op *fuseops.ReleaseFileHandleOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReleaseFileHandle(ctx, op)
	recordOp(ctx, "ReleaseFileHandle", startTime, err)
	return err
}

func (fs *monitoring) ReadSymlink(
	ctx context.Context,
	op *fuseops.ReadSymlinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReadSymlink(ctx, op)
	recordOp(ctx, "ReadSymlink", startTime, err)
	return err
}

func (fs *monitoring) RemoveXattr(
	ctx context.Context,
	op *fuseops.RemoveXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.RemoveXattr(ctx, op)
	recordOp(ctx, "RemoveXattr", startTime, err)
	return err
}

func (fs *monitoring) GetXattr(
	ctx context.Context,
	op *fuseops.GetXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.GetXattr(ctx, op)
	recordOp(ctx, "GetXattr", startTime, err)
	return err
}

func (fs *monitoring) ListXattr(
	ctx context.Context,
	op *fuseops.ListXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.ListXattr(ctx, op)
	recordOp(ctx, "ListXattr", startTime, err)
	return err
}

func (fs *monitoring) SetXattr(
	ctx context.Context,
	op *fuseops.SetXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.SetXattr(ctx, op)
	recordOp(ctx, "SetXattr", startTime, err)
	return err
}

func (fs *monitoring) Fallocate(
	ctx context.Context,
	op *fuseops.FallocateOp) error {
	startTime := time.Now()
	err := fs.wrapped.Fallocate(ctx, op)
	recordOp(ctx, "Fallocate", startTime, err)
	return err
}
<commit_msg>Shift error-type tag from opCount to opErrorCount<commit_after>// Copyright 2020 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package wrappers

import (
	"context"
	"errors"
	"fmt"
	"log"
	"syscall"
	"time"

	"github.com/googlecloudplatform/gcsfuse/internal/logger"
	"github.com/googlecloudplatform/gcsfuse/internal/monitor/tags"
	"github.com/jacobsa/fuse/fuseops"
	"github.com/jacobsa/fuse/fuseutil"
	"go.opencensus.io/plugin/ochttp"
	"go.opencensus.io/stats"
	"go.opencensus.io/stats/view"
	"go.opencensus.io/tag"
)

var errorLogger *log.Logger

var (
	opCount      = stats.Int64("fs/opCount", "The number of ops processed by the file system.", stats.UnitDimensionless)
	opLatency    = stats.Float64("fs/opLatency", "The latency of a file system operation.", stats.UnitMilliseconds)
	opErrorCount = stats.Int64("fs/opErrorCount", "The number of errors generated by file system operation.", stats.UnitDimensionless)
)

// Initialize the metrics.
func init() {

	errorLogger = logger.NewError("")

	// Register the view
	if err := view.Register(
		&view.View{
			Name:        "fs/opCount",
			Measure:     opCount,
			Description: "The cumulative number of ops processed by the file system.",
			Aggregation: view.Sum(),
			TagKeys:     []tag.Key{tags.FSOp},
		},
		&view.View{
			Name:        "fs/opErrorCount",
			Measure:     opErrorCount,
			Description: "The cumulative number of errors generated by file system operations",
			Aggregation: view.Sum(),
			TagKeys:     []tag.Key{tags.FSOp, tags.FSError},
		},
		&view.View{
			Name:        "fs/opLatency",
			Measure:     opLatency,
			Description: "The cumulative distribution of file system operation latencies",
			Aggregation: ochttp.DefaultLatencyDistribution,
			TagKeys:     []tag.Key{tags.FSOp},
		}); err != nil {
		fmt.Printf("Failed to register metrics for the file system: %v\n", err)
	}
}

// fsErrStr maps an error to a error string. Uncommon errors are aggregated to
// reduce the cardinality of the fs error to save the monitoring cost.
func fsErrStr(err error) string {
	if err == nil {
		return ""
	}
	var errno syscall.Errno
	if errors.As(err, &errno) {
		return errno.Error()
	}
	return DefaultFSError.Error()
}

// Records file system operation count, failed operation count and the operation latency
func recordOp(ctx context.Context, method string, start time.Time, fsErr error) {

	// Recording opCount
	if err := stats.RecordWithTags(
		ctx,
		[]tag.Mutator{
			tag.Upsert(tags.FSOp, method),
		},
		opCount.M(1),
	); err != nil {
		// Error in recording opCount
		fmt.Printf("Cannot record file system op: %v", err)
	}

	// Recording opErrorCount
	if fsErr != nil {
		if err := stats.RecordWithTags(
			ctx,
			[]tag.Mutator{
				tag.Upsert(tags.FSOp, method),
				tag.Upsert(tags.FSError, fsErrStr(fsErr)),
			},
			opErrorCount.M(1),
		); err != nil {
			// Error in recording opErrorCount
			errorLogger.Printf("Cannot record error count of the file system failed operations: %v", err)
		}
	}

	// Recording opLatency
	latencyMs := float64(time.Since(start).Microseconds()) / 1000.0
	if err := stats.RecordWithTags(
		ctx,
		[]tag.Mutator{
			tag.Upsert(tags.FSOp, method),
		},
		opLatency.M(latencyMs),
	); err != nil {
		// Error in opLatency
		errorLogger.Printf("Cannot record file system operation latency: %v", err)
	}
}

// WithMonitoring takes a FileSystem, returns a FileSystem with monitoring
// on the counts of requests per API.
func WithMonitoring(fs fuseutil.FileSystem) fuseutil.FileSystem {
	return &monitoring{
		wrapped: fs,
	}
}

type monitoring struct {
	wrapped fuseutil.FileSystem
}

func (fs *monitoring) Destroy() {
	fs.wrapped.Destroy()
}

func (fs *monitoring) StatFS(
	ctx context.Context,
	op *fuseops.StatFSOp) error {
	startTime := time.Now()
	err := fs.wrapped.StatFS(ctx, op)
	recordOp(ctx, "StatFS", startTime, err)
	return err
}

func (fs *monitoring) LookUpInode(
	ctx context.Context,
	op *fuseops.LookUpInodeOp) error {
	startTime := time.Now()
	err := fs.wrapped.LookUpInode(ctx, op)
	recordOp(ctx, "LookUpInode", startTime, err)
	return err
}

func (fs *monitoring) GetInodeAttributes(
	ctx context.Context,
	op *fuseops.GetInodeAttributesOp) error {
	startTime := time.Now()
	err := fs.wrapped.GetInodeAttributes(ctx, op)
	recordOp(ctx, "GetInodeAttributes", startTime, err)
	return err
}

func (fs *monitoring) SetInodeAttributes(
	ctx context.Context,
	op *fuseops.SetInodeAttributesOp) error {
	startTime := time.Now()
	err := fs.wrapped.SetInodeAttributes(ctx, op)
	recordOp(ctx, "SetInodeAttributes", startTime, err)
	return err
}

func (fs *monitoring) ForgetInode(
	ctx context.Context,
	op *fuseops.ForgetInodeOp) error {
	startTime := time.Now()
	err := fs.wrapped.ForgetInode(ctx, op)
	recordOp(ctx, "ForgetInode", startTime, err)
	return err
}

func (fs *monitoring) MkDir(
	ctx context.Context,
	op *fuseops.MkDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.MkDir(ctx, op)
	recordOp(ctx, "MkDir", startTime, err)
	return err
}

func (fs *monitoring) MkNode(
	ctx context.Context,
	op *fuseops.MkNodeOp) error {
	startTime := time.Now()
	err := fs.wrapped.MkNode(ctx, op)
	recordOp(ctx, "MkNode", startTime, err)
	return err
}

func (fs *monitoring) CreateFile(
	ctx context.Context,
	op *fuseops.CreateFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.CreateFile(ctx, op)
	recordOp(ctx, "CreateFile", startTime, err)
	return err
}

func (fs *monitoring) CreateLink(
	ctx context.Context,
	op *fuseops.CreateLinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.CreateLink(ctx, op)
	recordOp(ctx, "CreateLink", startTime, err)
	return err
}

func (fs *monitoring) CreateSymlink(
	ctx context.Context,
	op *fuseops.CreateSymlinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.CreateSymlink(ctx, op)
	recordOp(ctx, "CreateSymlink", startTime, err)
	return err
}

func (fs *monitoring) Rename(
	ctx context.Context,
	op *fuseops.RenameOp) error {
	startTime := time.Now()
	err := fs.wrapped.Rename(ctx, op)
	recordOp(ctx, "Rename", startTime, err)
	return err
}

func (fs *monitoring) RmDir(
	ctx context.Context,
	op *fuseops.RmDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.RmDir(ctx, op)
	recordOp(ctx, "RmDir", startTime, err)
	return err
}

func (fs *monitoring) Unlink(
	ctx context.Context,
	op *fuseops.UnlinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.Unlink(ctx, op)
	recordOp(ctx, "Unlink", startTime, err)
	return err
}

func (fs *monitoring) OpenDir(
	ctx context.Context,
	op *fuseops.OpenDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.OpenDir(ctx, op)
	recordOp(ctx, "OpenDir", startTime, err)
	return err
}

func (fs *monitoring) ReadDir(
	ctx context.Context,
	op *fuseops.ReadDirOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReadDir(ctx, op)
	recordOp(ctx, "ReadDir", startTime, err)
	return err
}

func (fs *monitoring) ReleaseDirHandle(
	ctx context.Context,
	op *fuseops.ReleaseDirHandleOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReleaseDirHandle(ctx, op)
	recordOp(ctx, "ReleaseDirHandle", startTime, err)
	return err
}

func (fs *monitoring) OpenFile(
	ctx context.Context,
	op *fuseops.OpenFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.OpenFile(ctx, op)
	recordOp(ctx, "OpenFile", startTime, err)
	return err
}

func (fs *monitoring) ReadFile(
	ctx context.Context,
	op *fuseops.ReadFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReadFile(ctx, op)
	recordOp(ctx, "ReadFile", startTime, err)
	return err
}

func (fs *monitoring) WriteFile(
	ctx context.Context,
	op *fuseops.WriteFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.WriteFile(ctx, op)
	recordOp(ctx, "WriteFile", startTime, err)
	return err
}

func (fs *monitoring) SyncFile(
	ctx context.Context,
	op *fuseops.SyncFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.SyncFile(ctx, op)
	recordOp(ctx, "SyncFile", startTime, err)
	return err
}

func (fs *monitoring) FlushFile(
	ctx context.Context,
	op *fuseops.FlushFileOp) error {
	startTime := time.Now()
	err := fs.wrapped.FlushFile(ctx, op)
	recordOp(ctx, "FlushFile", startTime, err)
	return err
}

func (fs *monitoring) ReleaseFileHandle(
	ctx context.Context,
	op *fuseops.ReleaseFileHandleOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReleaseFileHandle(ctx, op)
	recordOp(ctx, "ReleaseFileHandle", startTime, err)
	return err
}

func (fs *monitoring) ReadSymlink(
	ctx context.Context,
	op *fuseops.ReadSymlinkOp) error {
	startTime := time.Now()
	err := fs.wrapped.ReadSymlink(ctx, op)
	recordOp(ctx, "ReadSymlink", startTime, err)
	return err
}

func (fs *monitoring) RemoveXattr(
	ctx context.Context,
	op *fuseops.RemoveXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.RemoveXattr(ctx, op)
	recordOp(ctx, "RemoveXattr", startTime, err)
	return err
}

func (fs *monitoring) GetXattr(
	ctx context.Context,
	op *fuseops.GetXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.GetXattr(ctx, op)
	recordOp(ctx, "GetXattr", startTime, err)
	return err
}

func (fs *monitoring) ListXattr(
	ctx context.Context,
	op *fuseops.ListXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.ListXattr(ctx, op)
	recordOp(ctx, "ListXattr", startTime, err)
	return err
}

func (fs *monitoring) SetXattr(
	ctx context.Context,
	op *fuseops.SetXattrOp) error {
	startTime := time.Now()
	err := fs.wrapped.SetXattr(ctx, op)
	recordOp(ctx, "SetXattr", startTime, err)
	return err
}

func (fs *monitoring) Fallocate(
	ctx context.Context,
	op *fuseops.FallocateOp) error {
	startTime := time.Now()
	err := fs.wrapped.Fallocate(ctx, op)
	recordOp(ctx, "Fallocate", startTime, err)
	return err
}
<|endoftext|>


<commit_before>package frontend

import (
	"bytes"
	"encoding/base64"
	"io"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"

	"github.com/gorilla/mux"

	gw "github.com/cvmfs/gateway/internal/gateway"
	be "github.com/cvmfs/gateway/internal/gateway/backend"
)

type message map[string]interface{}

// MakeAuthzMiddleware returns an HMAC authorization middleware for use with the gorilla/mux server
func MakeAuthzMiddleware(ac be.ActionController) mux.MiddlewareFunc {
	return mux.MiddlewareFunc(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			// GET requests do not need authorization
			if req.Method == "GET" {
				next.ServeHTTP(w, req)
				return
			}

			ctx := req.Context()
			tokens := strings.Split(req.Header.Get("Authorization"), " ")
			if len(tokens) != 2 {
				gw.LogC(ctx, "http", gw.LogError).
					Msg("missing tokens in authorization header")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}

			keyID := tokens[0]
			HMAC, err := base64.StdEncoding.DecodeString(tokens[1])
			if err != nil {
				gw.LogC(ctx, "http", gw.LogError).
					Err(err).Msg("could not base64 decode HMAC")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}

			secret := ac.GetSecret(keyID)
			if len(secret) == 0 {
				gw.LogC(ctx, "http", gw.LogError).
					Msg("invalid key ID specified")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}

			// Different parts of the request are used to compute then HMAC, depending
			// in HTTP method and route

			var HMACInput []byte
			if strings.HasPrefix(req.URL.Path, APIRoot+"/leases") {
				token, _ := mux.Vars(req)["token"]
				if token != "" {
					// For commit/drop lease requests use the token to compute HMAC
					HMACInput = []byte(token)
				} else {
					// For new lease request used the request body to compute HMAC
					HMACInput, err = ioutil.ReadAll(req.Body)
					if err != nil {
						httpWrapError(ctx, err, "could not read request body", w, http.StatusInternalServerError)
						return
					}
					// Body needs to be read again in the next handler, reset it
					// using a copy of the original body
					bodyCopy := ioutil.NopCloser(bytes.NewReader(HMACInput))
					req.Body.Close()
					req.Body = bodyCopy
				}
			} else if strings.HasPrefix(req.URL.Path, APIRoot+"/payloads") {
				token, _ := mux.Vars(req)["token"]
				if token != "" {
					// For the new style of payload submission requests, use the token to compute HMAC
					HMACInput = []byte(token)
				} else {
					// For legacy payload submission requests, the JSON msg at the beginning of the body
					// is used to compute the HMAC
					sz := req.Header.Get("message-size")
					msgSize, err := strconv.Atoi(sz)
					if err != nil {
						httpWrapError(ctx, err, "missing message-size header", w, http.StatusBadRequest)
						return
					}
					msgRdr := io.LimitReader(req.Body, int64(msgSize))
					msg, err := ioutil.ReadAll(msgRdr)
					if err != nil {
						httpWrapError(ctx, err, "invalid request body", w, http.StatusBadRequest)
						return
					}

					HMACInput = msg

					// replace the request body with a new ReadCLoser which includes the already-read
					// head part
					req.Body = newRecombineReadCloser(msg, req.Body)
				}
			}

			if !CheckHMAC(HMACInput, HMAC, secret) {
				gw.LogC(ctx, "http", gw.LogError).
					Msg("invalid HMAC")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}
			next.ServeHTTP(w, req)
		})
	})
}

// The recombineReadCloser is used during payload submission requests to recombine the request message,
// already read inside the authorization middleware with the remaining request body and ensure that the
// body (io.ReadCloser) is eventually closed and does not leak
type recombineReadCloser struct {
	combined io.Reader
	original io.ReadCloser
}

func newRecombineReadCloser(head []byte, tail io.ReadCloser) *recombineReadCloser {
	return &recombineReadCloser{io.MultiReader(bytes.NewReader(head), tail), tail}
}

func (r recombineReadCloser) Read(p []byte) (int, error) {
	return r.combined.Read(p)
}

func (r recombineReadCloser) Close() error {
	return r.original.Close()
}
<commit_msg>Authz use io.ReadFull instead of io.LimitReader<commit_after>package frontend

import (
	"bytes"
	"encoding/base64"
	"io"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"

	"github.com/gorilla/mux"

	gw "github.com/cvmfs/gateway/internal/gateway"
	be "github.com/cvmfs/gateway/internal/gateway/backend"
)

type message map[string]interface{}

// MakeAuthzMiddleware returns an HMAC authorization middleware for use with the gorilla/mux server
func MakeAuthzMiddleware(ac be.ActionController) mux.MiddlewareFunc {
	return mux.MiddlewareFunc(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			// GET requests do not need authorization
			if req.Method == "GET" {
				next.ServeHTTP(w, req)
				return
			}

			ctx := req.Context()
			tokens := strings.Split(req.Header.Get("Authorization"), " ")
			if len(tokens) != 2 {
				gw.LogC(ctx, "http", gw.LogError).
					Msg("missing tokens in authorization header")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}

			keyID := tokens[0]
			HMAC, err := base64.StdEncoding.DecodeString(tokens[1])
			if err != nil {
				gw.LogC(ctx, "http", gw.LogError).
					Err(err).Msg("could not base64 decode HMAC")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}

			secret := ac.GetSecret(keyID)
			if len(secret) == 0 {
				gw.LogC(ctx, "http", gw.LogError).
					Msg("invalid key ID specified")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}

			// Different parts of the request are used to compute then HMAC, depending
			// in HTTP method and route

			var HMACInput []byte
			if strings.HasPrefix(req.URL.Path, APIRoot+"/leases") {
				token, _ := mux.Vars(req)["token"]
				if token != "" {
					// For commit/drop lease requests use the token to compute HMAC
					HMACInput = []byte(token)
				} else {
					// For new lease request used the request body to compute HMAC
					HMACInput, err = ioutil.ReadAll(req.Body)
					if err != nil {
						httpWrapError(ctx, err, "could not read request body", w, http.StatusInternalServerError)
						return
					}
					// Body needs to be read again in the next handler, reset it
					// using a copy of the original body
					bodyCopy := ioutil.NopCloser(bytes.NewReader(HMACInput))
					req.Body.Close()
					req.Body = bodyCopy
				}
			} else if strings.HasPrefix(req.URL.Path, APIRoot+"/payloads") {
				token, _ := mux.Vars(req)["token"]
				if token != "" {
					// For the new style of payload submission requests, use the token to compute HMAC
					HMACInput = []byte(token)
				} else {
					// For legacy payload submission requests, the JSON msg at the beginning of the body
					// is used to compute the HMAC
					sz := req.Header.Get("message-size")
					msgSize, err := strconv.Atoi(sz)
					if err != nil {
						httpWrapError(ctx, err, "missing message-size header", w, http.StatusBadRequest)
						return
					}
					msg := make([]byte, msgSize)
					if _, err := io.ReadFull(req.Body, msg); err != nil {
						httpWrapError(ctx, err, "invalid request body", w, http.StatusBadRequest)
						return
					}

					HMACInput = msg

					// replace the request body with a new ReadCLoser which includes the already-read
					// head part
					req.Body = newRecombineReadCloser(msg, req.Body)
				}
			}

			if !CheckHMAC(HMACInput, HMAC, secret) {
				gw.LogC(ctx, "http", gw.LogError).
					Msg("invalid HMAC")
				replyJSON(ctx, w, message{"status": "error", "reason": "invalid_hmac"})
				return
			}
			next.ServeHTTP(w, req)
		})
	})
}

// The recombineReadCloser is used during payload submission requests to recombine the request message,
// already read inside the authorization middleware with the remaining request body and ensure that the
// body (io.ReadCloser) is eventually closed and does not leak
type recombineReadCloser struct {
	combined io.Reader
	original io.ReadCloser
}

func newRecombineReadCloser(head []byte, tail io.ReadCloser) *recombineReadCloser {
	return &recombineReadCloser{io.MultiReader(bytes.NewReader(head), tail), tail}
}

func (r recombineReadCloser) Read(p []byte) (int, error) {
	return r.combined.Read(p)
}

func (r recombineReadCloser) Close() error {
	return r.original.Close()
}
<|endoftext|>


<commit_before>package nametemplate

import (
	"testing"

	"github.com/goreleaser/goreleaser/config"
	"github.com/goreleaser/goreleaser/context"
	"github.com/goreleaser/goreleaser/internal/artifact"
	"github.com/tj/assert"
)

func TestNameTemplate(t *testing.T) {
	var ctx = context.New(config.Project{
		ProjectName: "proj",
		Archive: config.Archive{
			NameTemplate: "{{.Binary}}_{{.ProjectName}}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}",
			Replacements: map[string]string{
				"windows": "windao",
			},
		},
	})
	s, err := Apply(ctx, artifact.Artifact{
		Goos:   "windows",
		Goarch: "amd64",
		Name:   "winbin",
	}, "bin")
	assert.NoError(t, err)
	assert.Equal(t, "bin_bin_windao_amd64", s)
	s, err = Apply(ctx, artifact.Artifact{
		Goos:   "darwin",
		Goarch: "amd64",
		Name:   "winbin",
	}, "bin")
	assert.NoError(t, err)
	assert.Equal(t, "bin_bin_darwin_amd64", s)
}

func TestInvalidNameTemplate(t *testing.T) {
	var ctx = context.New(config.Project{
		ProjectName: "proj",
		Archive: config.Archive{
			NameTemplate: "{{.Binary}",
		},
	})
	s, err := Apply(ctx, artifact.Artifact{
		Goos:   "windows",
		Goarch: "amd64",
		Name:   "winbin",
	}, "bin")
	assert.EqualError(t, err, `template: archive_name:1: unexpected "}" in operand`)
	assert.Empty(t, s)
}
<commit_msg>test: testing log output as well<commit_after>package nametemplate

import (
	"bytes"
	"testing"

	"github.com/apex/log"
	"github.com/apex/log/handlers/cli"
	"github.com/goreleaser/goreleaser/config"
	"github.com/goreleaser/goreleaser/context"
	"github.com/goreleaser/goreleaser/internal/artifact"
	"github.com/tj/assert"
)

func TestNameTemplate(t *testing.T) {
	var ctx = context.New(config.Project{
		ProjectName: "proj",
		Archive: config.Archive{
			NameTemplate: "{{.Binary}}_{{.ProjectName}}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}",
			Replacements: map[string]string{
				"windows": "windao",
			},
		},
	})
	s, err := Apply(ctx, artifact.Artifact{
		Goos:   "windows",
		Goarch: "amd64",
		Name:   "winbin",
	}, "bin")
	assert.NoError(t, err)
	assert.Equal(t, "bin_bin_windao_amd64", s)
	s, err = Apply(ctx, artifact.Artifact{
		Goos:   "darwin",
		Goarch: "amd64",
		Name:   "winbin",
	}, "bin")
	assert.NoError(t, err)
	assert.Equal(t, "bin_bin_darwin_amd64", s)
}

func TestInvalidNameTemplate(t *testing.T) {
	var ctx = context.New(config.Project{
		ProjectName: "proj",
		Archive: config.Archive{
			NameTemplate: "{{.Binary}",
		},
	})
	s, err := Apply(ctx, artifact.Artifact{
		Goos:   "windows",
		Goarch: "amd64",
		Name:   "winbin",
	}, "bin")
	assert.EqualError(t, err, `template: archive_name:1: unexpected "}" in operand`)
	assert.Empty(t, s)
}

func TestDeprecatedFieldOnNameTemplate(t *testing.T) {
	for _, temp := range []string{
		"{{.Binary}}",
		"{{ .Binary}}",
		"{{.Binary }}",
		"{{ .Binary }}",
	} {
		t.Run(temp, func(tt *testing.T) {
			var out bytes.Buffer
			log.SetHandler(cli.New(&out))
			var ctx = context.New(config.Project{
				ProjectName: "proj",
				Archive: config.Archive{
					NameTemplate: temp,
				},
			})
			s, err := Apply(ctx, artifact.Artifact{
				Goos:   "windows",
				Goarch: "amd64",
				Name:   "winbin",
			}, "bin")
			assert.NoError(tt, err)
			assert.Equal(tt, "bin", s)
			assert.Contains(tt, out.String(), "you are using a deprecated field on your template")
		})
	}
}
<|endoftext|>


<commit_before>package siegreader

import (
	"io"
	"os"
	"strings"
	"testing"
)

func TestRead(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	buf := make([]byte, 62)
	i, err := r.Read(buf)
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if i != 62 {
		t.Errorf("Read error: expecting a read length of 62, got %v", i)
	}
	if string(buf) != testString {
		t.Errorf("Read error: %s should equal %s", string(buf), testString)
	}
	bufs.Put(b)
}

func readAt(t *testing.T, r *Reader) {
	buf := make([]byte, 5)
	i, err := r.ReadAt(buf, 4)
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if i != 5 {
		t.Errorf("Read error: expecting a read length of 5, got %d", i)
	}
	if string(buf) != "45678" {
		t.Errorf("Read error: %s should equal %s", string(buf), "45678")
	}
}

func TestReadAt(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	readAt(t, r)
	bufs.Put(b)
}

func readByte(t *testing.T, r *Reader) {
	c, err := r.ReadByte()
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if c != '0' {
		t.Errorf("Read error: expecting '0', got %s", string(c))
	}
	c, err = r.ReadByte()
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if c != '1' {
		t.Errorf("Read error: expecting '1', got %s", string(c))
	}
}

func TestReadByte(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	readByte(t, r)
	bufs.Put(b)
}

func seek(t *testing.T, r *Reader) {
	_, err := r.Seek(6, 0)
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	c, err := r.ReadByte()
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if c != '6' {
		t.Errorf("Read error: expecting '6', got %s", string(c))
	}

}

func TestSeek(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	seek(t, r)
	bufs.Put(b)
}

func TestReuse(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b, err := bufs.Get(r)
	if err != nil {
		t.Fatal(err)
	}
	bufs.Put(b)
	r.Close()
	nr := strings.NewReader(testString)
	q := make(chan struct{})
	b, err = bufs.Get(nr)
	if err != nil && err != io.EOF {
		t.Fatal(err)
	}
	b.Quit = q
	if err != nil && err != io.EOF {
		t.Errorf("Read error: %v", err)
	}
	reuse := ReaderFrom(b)
	readByte(t, reuse)
	seek(t, reuse)
	bufs.Put(b)
}

func drain(r io.ByteReader, results chan int) {
	var i int
	for _, e := r.ReadByte(); e == nil; _, e = r.ReadByte() {
		i++
	}
	results <- i
}

func TestDrain(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	results := make(chan int)
	go drain(r, results)
	if i := <-results; i != 62 {
		t.Errorf("Expecting 62, got %v", i)
	}
	bufs.Put(b)
}

func TestDrainFile(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b := setup(r, t)
	first := ReaderFrom(b)
	results := make(chan int)
	go drain(first, results)
	if i := <-results; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	r.Close()
	bufs.Put(b)
}

func TestMultiple(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b := setup(r, t)
	first := ReaderFrom(b)
	second := ReaderFrom(b)
	results := make(chan int)
	go drain(first, results)
	go drain(second, results)
	if i := <-results; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	if i := <-results; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	bufs.Put(b)
}

func TestReverse(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReverseReaderFrom(b)
	first := ReaderFrom(b)
	results := make(chan int)
	go drain(first, results)
	<-results
	c, err := r.ReadByte()
	if err != nil {
		t.Fatalf("Read error: %v", err)
	}
	if c != 'Z' {
		t.Fatalf("Read error: expecting 'Z', got %s", string(c))
	}
	c, err = r.ReadByte()
	if err != nil {
		t.Fatalf("Read error: %v", err)
	}
	if c != 'Y' {
		t.Fatalf("Read error: expecting 'Y', got %s", string(c))
	}
	bufs.Put(b)
}

func TestReverseDrainFile(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b := setup(r, t)
	quit := make(chan struct{})
	b.Quit = quit
	first := ReaderFrom(b)
	firstResults := make(chan int, 1)
	last := ReverseReaderFrom(b)
	lastResults := make(chan int)
	go drain(first, firstResults)
	go drain(last, lastResults)
	if i := <-lastResults; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	r.Close()
	bufs.Put(b)
}

func TestLimit(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := LimitReaderFrom(b, 5)
	results := make(chan int)
	go drain(r, results)
	if i := <-results; i != 5 {
		t.Errorf("Expecting 5, got %d", i)
	}
	bufs.Put(b)
}

func TestReverseLimit(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	l := LimitReaderFrom(b, -1)
	firstResults := make(chan int, 1)
	go drain(l, firstResults)
	r := LimitReverseReaderFrom(b, 5)
	results := make(chan int)
	go drain(r, results)
	if i := <-results; i != 5 {
		t.Errorf("Expecting 5, got %d", i)
	}
	bufs.Put(b)
}
<commit_msg>test fails if close reader before drained<commit_after>package siegreader

import (
	"io"
	"os"
	"strings"
	"testing"
)

func TestRead(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	buf := make([]byte, 62)
	i, err := r.Read(buf)
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if i != 62 {
		t.Errorf("Read error: expecting a read length of 62, got %v", i)
	}
	if string(buf) != testString {
		t.Errorf("Read error: %s should equal %s", string(buf), testString)
	}
	bufs.Put(b)
}

func readAt(t *testing.T, r *Reader) {
	buf := make([]byte, 5)
	i, err := r.ReadAt(buf, 4)
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if i != 5 {
		t.Errorf("Read error: expecting a read length of 5, got %d", i)
	}
	if string(buf) != "45678" {
		t.Errorf("Read error: %s should equal %s", string(buf), "45678")
	}
}

func TestReadAt(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	readAt(t, r)
	bufs.Put(b)
}

func readByte(t *testing.T, r *Reader) {
	c, err := r.ReadByte()
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if c != '0' {
		t.Errorf("Read error: expecting '0', got %s", string(c))
	}
	c, err = r.ReadByte()
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if c != '1' {
		t.Errorf("Read error: expecting '1', got %s", string(c))
	}
}

func TestReadByte(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	readByte(t, r)
	bufs.Put(b)
}

func seek(t *testing.T, r *Reader) {
	_, err := r.Seek(6, 0)
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	c, err := r.ReadByte()
	if err != nil {
		t.Errorf("Read error: %v", err)
	}
	if c != '6' {
		t.Errorf("Read error: expecting '6', got %s", string(c))
	}

}

func TestSeek(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	seek(t, r)
	bufs.Put(b)
}

func TestReuse(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b, err := bufs.Get(r)
	if err != nil {
		t.Fatal(err)
	}
	bufs.Put(b)
	r.Close()
	nr := strings.NewReader(testString)
	q := make(chan struct{})
	b, err = bufs.Get(nr)
	if err != nil && err != io.EOF {
		t.Fatal(err)
	}
	b.Quit = q
	if err != nil && err != io.EOF {
		t.Errorf("Read error: %v", err)
	}
	reuse := ReaderFrom(b)
	readByte(t, reuse)
	seek(t, reuse)
	bufs.Put(b)
}

func drain(r io.ByteReader, results chan int) {
	var i int
	for _, e := r.ReadByte(); e == nil; _, e = r.ReadByte() {
		i++
	}
	results <- i
}

func TestDrain(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReaderFrom(b)
	results := make(chan int)
	go drain(r, results)
	if i := <-results; i != 62 {
		t.Errorf("Expecting 62, got %v", i)
	}
	bufs.Put(b)
}

func TestDrainFile(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b := setup(r, t)
	first := ReaderFrom(b)
	results := make(chan int)
	go drain(first, results)
	if i := <-results; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	r.Close()
	bufs.Put(b)
}

func TestMultiple(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b := setup(r, t)
	first := ReaderFrom(b)
	second := ReaderFrom(b)
	results := make(chan int)
	go drain(first, results)
	go drain(second, results)
	if i := <-results; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	if i := <-results; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	bufs.Put(b)
}

func TestReverse(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := ReverseReaderFrom(b)
	first := ReaderFrom(b)
	results := make(chan int)
	go drain(first, results)
	<-results
	c, err := r.ReadByte()
	if err != nil {
		t.Fatalf("Read error: %v", err)
	}
	if c != 'Z' {
		t.Fatalf("Read error: expecting 'Z', got %s", string(c))
	}
	c, err = r.ReadByte()
	if err != nil {
		t.Fatalf("Read error: %v", err)
	}
	if c != 'Y' {
		t.Fatalf("Read error: expecting 'Y', got %s", string(c))
	}
	bufs.Put(b)
}

func TestReverseDrainFile(t *testing.T) {
	r, err := os.Open(testfile)
	if err != nil {
		t.Fatal(err)
	}
	b := setup(r, t)
	quit := make(chan struct{})
	b.Quit = quit
	first := ReaderFrom(b)
	firstResults := make(chan int, 1)
	last := ReverseReaderFrom(b)
	lastResults := make(chan int)
	go drain(first, firstResults)
	go drain(last, lastResults)
	if i := <-lastResults; i != 24040 {
		t.Errorf("Expecting 24040, got %v", i)
	}
	<- firstResults // make sure we've finished in both directions before closing file
	r.Close()
	bufs.Put(b)
}

func TestLimit(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	r := LimitReaderFrom(b, 5)
	results := make(chan int)
	go drain(r, results)
	if i := <-results; i != 5 {
		t.Errorf("Expecting 5, got %d", i)
	}
	bufs.Put(b)
}

func TestReverseLimit(t *testing.T) {
	b := setup(strings.NewReader(testString), t)
	l := LimitReaderFrom(b, -1)
	firstResults := make(chan int, 1)
	go drain(l, firstResults)
	r := LimitReverseReaderFrom(b, 5)
	results := make(chan int)
	go drain(r, results)
	if i := <-results; i != 5 {
		t.Errorf("Expecting 5, got %d", i)
	}
	bufs.Put(b)
}
<|endoftext|>


<commit_before>/*
 *
 * k6 - a next-generation load testing tool
 * Copyright (C) 2016 Load Impact
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package lib

import (
	"context"
	"github.com/stretchr/testify/assert"
	"gopkg.in/guregu/null.v3"
	"runtime"
	"testing"
	"time"
)

func TestNewEngine(t *testing.T) {
	_, err := NewEngine(nil, Options{})
	assert.NoError(t, err)
}

func TestNewEngineOptions(t *testing.T) {
	t.Run("VUsMax", func(t *testing.T) {
		t.Run("not set", func(t *testing.T) {
			e, err := NewEngine(nil, Options{})
			assert.NoError(t, err)
			assert.Equal(t, int64(0), e.GetVUsMax())
			assert.Equal(t, int64(0), e.GetVUs())
		})
		t.Run("set", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(10),
			})
			assert.NoError(t, err)
			assert.Equal(t, int64(10), e.GetVUsMax())
			assert.Equal(t, int64(0), e.GetVUs())
		})
	})
	t.Run("VUs", func(t *testing.T) {
		t.Run("no max", func(t *testing.T) {
			_, err := NewEngine(nil, Options{
				VUs: null.IntFrom(10),
			})
			assert.EqualError(t, err, "more vus than allocated requested")
		})
		t.Run("max too low", func(t *testing.T) {
			_, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(1),
				VUs:    null.IntFrom(10),
			})
			assert.EqualError(t, err, "more vus than allocated requested")
		})
		t.Run("max higher", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(10),
				VUs:    null.IntFrom(1),
			})
			assert.NoError(t, err)
			assert.Equal(t, int64(10), e.GetVUsMax())
			assert.Equal(t, int64(1), e.GetVUs())
		})
		t.Run("max just right", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(10),
				VUs:    null.IntFrom(10),
			})
			assert.NoError(t, err)
			assert.Equal(t, int64(10), e.GetVUsMax())
			assert.Equal(t, int64(10), e.GetVUs())
		})
	})
	t.Run("Paused", func(t *testing.T) {
		t.Run("not set", func(t *testing.T) {
			e, err := NewEngine(nil, Options{})
			assert.NoError(t, err)
			assert.False(t, e.IsPaused())
		})
		t.Run("false", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				Paused: null.BoolFrom(false),
			})
			assert.NoError(t, err)
			assert.False(t, e.IsPaused())
		})
		t.Run("true", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				Paused: null.BoolFrom(true),
			})
			assert.NoError(t, err)
			assert.True(t, e.IsPaused())
		})
	})
}

func TestEngineRun(t *testing.T) {
	t.Run("exits with context", func(t *testing.T) {
		startTime := time.Now()
		duration := 100 * time.Millisecond
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)

		ctx, _ := context.WithTimeout(context.Background(), duration)
		assert.NoError(t, e.Run(ctx))
		assert.WithinDuration(t, startTime.Add(duration), time.Now(), 100*time.Millisecond)
	})
	t.Run("terminates subctx", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)

		subctx := e.subctx
		select {
		case <-subctx.Done():
			assert.Fail(t, "context is already terminated")
		default:
		}

		ctx, cancel := context.WithCancel(context.Background())
		cancel()
		e.Run(ctx)

		assert.NotEqual(t, subctx, e.subctx, "subcontext not changed")
		select {
		case <-subctx.Done():
		default:
			assert.Fail(t, "context was not terminated")
		}
	})
}

func TestEngineIsRunning(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	e, err := NewEngine(nil, Options{})
	assert.NoError(t, err)

	go func() { assert.NoError(t, e.Run(ctx)) }()
	runtime.Gosched()
	assert.True(t, e.IsRunning())

	cancel()
	runtime.Gosched()
	assert.False(t, e.IsRunning())
}

func TestEngineSetPaused(t *testing.T) {
	e, err := NewEngine(nil, Options{})
	assert.NoError(t, err)
	assert.False(t, e.IsPaused())

	e.SetPaused(true)
	assert.True(t, e.IsPaused())

	e.SetPaused(false)
	assert.False(t, e.IsPaused())
}

func TestEngineSetVUsMax(t *testing.T) {
	t.Run("not set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.Equal(t, int64(0), e.GetVUsMax())
	})
	t.Run("set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.NoError(t, e.SetVUsMax(10))
		assert.Equal(t, int64(10), e.GetVUsMax())
	})
	t.Run("set negative", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.EqualError(t, e.SetVUsMax(-1), "vus-max can't be negative")
	})
	t.Run("set too low", func(t *testing.T) {
		e, err := NewEngine(nil, Options{
			VUsMax: null.IntFrom(10),
			VUs:    null.IntFrom(10),
		})
		assert.NoError(t, err)
		assert.EqualError(t, e.SetVUsMax(5), "can't reduce vus-max below vus")
	})
}

func TestEngineSetVUs(t *testing.T) {
	t.Run("not set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.Equal(t, int64(0), e.GetVUsMax())
		assert.Equal(t, int64(0), e.GetVUs())
	})
	t.Run("set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{VUsMax: null.IntFrom(10)})
		assert.NoError(t, err)
		assert.NoError(t, e.SetVUs(10))
		assert.Equal(t, int64(10), e.GetVUs())
	})
	t.Run("set too high", func(t *testing.T) {
		e, err := NewEngine(nil, Options{VUsMax: null.IntFrom(10)})
		assert.NoError(t, err)
		assert.EqualError(t, e.SetVUs(20), "more vus than allocated requested")
	})
}
<commit_msg>[test/fix] Don't ignore it here either<commit_after>/*
 *
 * k6 - a next-generation load testing tool
 * Copyright (C) 2016 Load Impact
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package lib

import (
	"context"
	"github.com/stretchr/testify/assert"
	"gopkg.in/guregu/null.v3"
	"runtime"
	"testing"
	"time"
)

func TestNewEngine(t *testing.T) {
	_, err := NewEngine(nil, Options{})
	assert.NoError(t, err)
}

func TestNewEngineOptions(t *testing.T) {
	t.Run("VUsMax", func(t *testing.T) {
		t.Run("not set", func(t *testing.T) {
			e, err := NewEngine(nil, Options{})
			assert.NoError(t, err)
			assert.Equal(t, int64(0), e.GetVUsMax())
			assert.Equal(t, int64(0), e.GetVUs())
		})
		t.Run("set", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(10),
			})
			assert.NoError(t, err)
			assert.Equal(t, int64(10), e.GetVUsMax())
			assert.Equal(t, int64(0), e.GetVUs())
		})
	})
	t.Run("VUs", func(t *testing.T) {
		t.Run("no max", func(t *testing.T) {
			_, err := NewEngine(nil, Options{
				VUs: null.IntFrom(10),
			})
			assert.EqualError(t, err, "more vus than allocated requested")
		})
		t.Run("max too low", func(t *testing.T) {
			_, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(1),
				VUs:    null.IntFrom(10),
			})
			assert.EqualError(t, err, "more vus than allocated requested")
		})
		t.Run("max higher", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(10),
				VUs:    null.IntFrom(1),
			})
			assert.NoError(t, err)
			assert.Equal(t, int64(10), e.GetVUsMax())
			assert.Equal(t, int64(1), e.GetVUs())
		})
		t.Run("max just right", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				VUsMax: null.IntFrom(10),
				VUs:    null.IntFrom(10),
			})
			assert.NoError(t, err)
			assert.Equal(t, int64(10), e.GetVUsMax())
			assert.Equal(t, int64(10), e.GetVUs())
		})
	})
	t.Run("Paused", func(t *testing.T) {
		t.Run("not set", func(t *testing.T) {
			e, err := NewEngine(nil, Options{})
			assert.NoError(t, err)
			assert.False(t, e.IsPaused())
		})
		t.Run("false", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				Paused: null.BoolFrom(false),
			})
			assert.NoError(t, err)
			assert.False(t, e.IsPaused())
		})
		t.Run("true", func(t *testing.T) {
			e, err := NewEngine(nil, Options{
				Paused: null.BoolFrom(true),
			})
			assert.NoError(t, err)
			assert.True(t, e.IsPaused())
		})
	})
}

func TestEngineRun(t *testing.T) {
	t.Run("exits with context", func(t *testing.T) {
		startTime := time.Now()
		duration := 100 * time.Millisecond
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)

		ctx, _ := context.WithTimeout(context.Background(), duration)
		assert.NoError(t, e.Run(ctx))
		assert.WithinDuration(t, startTime.Add(duration), time.Now(), 100*time.Millisecond)
	})
	t.Run("terminates subctx", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)

		subctx := e.subctx
		select {
		case <-subctx.Done():
			assert.Fail(t, "context is already terminated")
		default:
		}

		ctx, cancel := context.WithCancel(context.Background())
		cancel()
		assert.NoError(t, e.Run(ctx))

		assert.NotEqual(t, subctx, e.subctx, "subcontext not changed")
		select {
		case <-subctx.Done():
		default:
			assert.Fail(t, "context was not terminated")
		}
	})
}

func TestEngineIsRunning(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	e, err := NewEngine(nil, Options{})
	assert.NoError(t, err)

	go func() { assert.NoError(t, e.Run(ctx)) }()
	runtime.Gosched()
	assert.True(t, e.IsRunning())

	cancel()
	runtime.Gosched()
	assert.False(t, e.IsRunning())
}

func TestEngineSetPaused(t *testing.T) {
	e, err := NewEngine(nil, Options{})
	assert.NoError(t, err)
	assert.False(t, e.IsPaused())

	e.SetPaused(true)
	assert.True(t, e.IsPaused())

	e.SetPaused(false)
	assert.False(t, e.IsPaused())
}

func TestEngineSetVUsMax(t *testing.T) {
	t.Run("not set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.Equal(t, int64(0), e.GetVUsMax())
	})
	t.Run("set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.NoError(t, e.SetVUsMax(10))
		assert.Equal(t, int64(10), e.GetVUsMax())
	})
	t.Run("set negative", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.EqualError(t, e.SetVUsMax(-1), "vus-max can't be negative")
	})
	t.Run("set too low", func(t *testing.T) {
		e, err := NewEngine(nil, Options{
			VUsMax: null.IntFrom(10),
			VUs:    null.IntFrom(10),
		})
		assert.NoError(t, err)
		assert.EqualError(t, e.SetVUsMax(5), "can't reduce vus-max below vus")
	})
}

func TestEngineSetVUs(t *testing.T) {
	t.Run("not set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{})
		assert.NoError(t, err)
		assert.Equal(t, int64(0), e.GetVUsMax())
		assert.Equal(t, int64(0), e.GetVUs())
	})
	t.Run("set", func(t *testing.T) {
		e, err := NewEngine(nil, Options{VUsMax: null.IntFrom(10)})
		assert.NoError(t, err)
		assert.NoError(t, e.SetVUs(10))
		assert.Equal(t, int64(10), e.GetVUs())
	})
	t.Run("set too high", func(t *testing.T) {
		e, err := NewEngine(nil, Options{VUsMax: null.IntFrom(10)})
		assert.NoError(t, err)
		assert.EqualError(t, e.SetVUs(20), "more vus than allocated requested")
	})
}
<|endoftext|>


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

package event

import (
	batch_v1 "k8s.io/api/batch/v1"
	api_v1 "k8s.io/api/core/v1"
	"k8s.io/api/extensions/v1beta1"
)

// Event represent an event got from k8s api server
// Events from different endpoints need to be casted to KubewatchEvent
// before being able to be handled by handler
type Event struct {
	Namespace string
	Kind      string
	Component string
	Host      string
	Reason    string
	Status    string
	Name      string
}

var m = map[string]string{
	"created": "Normal",
	"deleted": "Danger",
	"updated": "Warning",
}

// New create new KubewatchEvent
func New(obj interface{}, action string) Event {
	var namespace, kind, component, host, reason, status, name string
	if apiService, ok := obj.(*api_v1.Service); ok {
		namespace = apiService.ObjectMeta.Namespace
		name = apiService.Name
		kind = "service"
		component = string(apiService.Spec.Type)
		reason = action
		status = m[action]
	} else if apiNamespace, ok := obj.(*api_v1.Namespace); ok {
		name = apiNamespace.Name
		kind = "namespace"
		reason = action
		status = m[action]
	} else if apiPod, ok := obj.(*api_v1.Pod); ok {
		namespace = apiPod.ObjectMeta.Namespace
		name = apiPod.Name
		kind = "pod"
		reason = action
		host = apiPod.Spec.NodeName
		status = m[action]
	} else if apiRC, ok := obj.(*api_v1.ReplicationController); ok {
		namespace = apiRC.ObjectMeta.Namespace
		name = apiRC.Name
		kind = "replication controller"
		reason = action
		status = m[action]
	} else if apiDeployment, ok := obj.(*v1beta1.Deployment); ok {
		namespace = apiDeployment.ObjectMeta.Namespace
		name = apiDeployment.Name
		kind = "deployment"
		reason = action
		status = m[action]
	} else if apiJob, ok := obj.(*batch_v1.Job); ok {
		namespace = apiJob.ObjectMeta.Namespace
		name = apiJob.Name
		kind = "job"
		reason = action
		status = m[action]
	} else if apiPV, ok := obj.(*api_v1.PersistentVolume); ok {
		name = apiPV.Name
		kind = "persistent volume"
		reason = action
		status = m[action]
	}

	kbEvent := Event{
		Namespace: namespace,
		Kind:      kind,
		Component: component,
		Host:      host,
		Reason:    reason,
		Status:    status,
		Name:      name,
	}

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

package event

import (
	apps_v1beta1 "k8s.io/api/apps/v1beta1"
	batch_v1 "k8s.io/api/batch/v1"
	api_v1 "k8s.io/api/core/v1"
)

// Event represent an event got from k8s api server
// Events from different endpoints need to be casted to KubewatchEvent
// before being able to be handled by handler
type Event struct {
	Namespace string
	Kind      string
	Component string
	Host      string
	Reason    string
	Status    string
	Name      string
}

var m = map[string]string{
	"created": "Normal",
	"deleted": "Danger",
	"updated": "Warning",
}

// New create new KubewatchEvent
func New(obj interface{}, action string) Event {
	var namespace, kind, component, host, reason, status, name string
	if apiService, ok := obj.(*api_v1.Service); ok {
		namespace = apiService.ObjectMeta.Namespace
		name = apiService.Name
		kind = "service"
		component = string(apiService.Spec.Type)
		reason = action
		status = m[action]
	} else if apiNamespace, ok := obj.(*api_v1.Namespace); ok {
		name = apiNamespace.Name
		kind = "namespace"
		reason = action
		status = m[action]
	} else if apiPod, ok := obj.(*api_v1.Pod); ok {
		namespace = apiPod.ObjectMeta.Namespace
		name = apiPod.Name
		kind = "pod"
		reason = action
		host = apiPod.Spec.NodeName
		status = m[action]
	} else if apiRC, ok := obj.(*api_v1.ReplicationController); ok {
		namespace = apiRC.ObjectMeta.Namespace
		name = apiRC.Name
		kind = "replication controller"
		reason = action
		status = m[action]
	} else if apiDeployment, ok := obj.(*apps_v1beta1.Deployment); ok {
		namespace = apiDeployment.ObjectMeta.Namespace
		name = apiDeployment.Name
		kind = "deployment"
		reason = action
		status = m[action]
	} else if apiJob, ok := obj.(*batch_v1.Job); ok {
		namespace = apiJob.ObjectMeta.Namespace
		name = apiJob.Name
		kind = "job"
		reason = action
		status = m[action]
	} else if apiPV, ok := obj.(*api_v1.PersistentVolume); ok {
		name = apiPV.Name
		kind = "persistent volume"
		reason = action
		status = m[action]
	}

	kbEvent := Event{
		Namespace: namespace,
		Kind:      kind,
		Component: component,
		Host:      host,
		Reason:    reason,
		Status:    status,
		Name:      name,
	}

	return kbEvent
}
<|endoftext|>


<commit_before>package gtr

import (
	"fmt"
	"time"
)

// ReportBuilder builds Reports.
type ReportBuilder struct {
	packages    []Package
	tests       map[int]Test
	benchmarks  map[int]Benchmark
	buildErrors map[int]Error
	runErrors   map[int]Error

	// state
	nextId   int // next free id
	lastId   int // last test id // TODO: stack?
	output   []string
	coverage float64

	// defaults
	packageName string
}

func NewReportBuilder(packageName string) *ReportBuilder {
	return &ReportBuilder{
		tests:       make(map[int]Test),
		benchmarks:  make(map[int]Benchmark),
		buildErrors: make(map[int]Error),
		runErrors:   make(map[int]Error),
		nextId:      1,
		packageName: packageName,
	}
}

func (b *ReportBuilder) newId() int {
	id := b.nextId
	b.lastId = id
	b.nextId += 1
	return id
}

func (b *ReportBuilder) flush() {
	// Create package in case we have tests or benchmarks that didn't get a
	// summary.
	if len(b.tests) > 0 || len(b.benchmarks) > 0 {
		b.CreatePackage(b.packageName, "", 0, "")
	}
}

func (b *ReportBuilder) Build() Report {
	b.flush()
	return Report{Packages: b.packages}
}

func (b *ReportBuilder) CreateTest(name string) {
	b.tests[b.newId()] = Test{Name: name}
}

func (b *ReportBuilder) PauseTest(name string) {
	b.lastId = 0
}

func (b *ReportBuilder) ContinueTest(name string) {
	b.lastId = b.findTest(name)
}

func (b *ReportBuilder) EndTest(name, result string, duration time.Duration) {
	id := b.findTest(name)
	b.lastId = id

	t := b.tests[id]
	t.Result = parseResult(result)
	t.Duration = duration
	b.tests[id] = t
}

func (b *ReportBuilder) End() {
	b.lastId = 0
}

func (b *ReportBuilder) Benchmark(name string, iterations int64, nsPerOp, mbPerSec float64, bytesPerOp, allocsPerOp int64) {
	b.benchmarks[b.newId()] = Benchmark{
		Name:        name,
		Result:      Pass,
		Iterations:  iterations,
		NsPerOp:     nsPerOp,
		MBPerSec:    mbPerSec,
		BytesPerOp:  bytesPerOp,
		AllocsPerOp: allocsPerOp,
	}
}

func (b *ReportBuilder) CreateBuildError(packageName string) {
	b.buildErrors[b.newId()] = Error{Name: packageName}
}

func (b *ReportBuilder) CreatePackage(name, result string, duration time.Duration, data string) {
	// Build errors are treated somewhat differently. Rather than having a
	// single package with all build errors collected so far, we only care
	// about the build errors for this particular package.
	for id, buildErr := range b.buildErrors {
		if buildErr.Name == name {
			if len(b.tests) > 0 || len(b.benchmarks) > 0 {
				panic("unexpected tests and/or benchmarks found in build error package")
			}
			buildErr.Duration = duration
			buildErr.Cause = data
			b.packages = append(b.packages, Package{
				Name:       name,
				BuildError: buildErr,
			})
			delete(b.buildErrors, id)
			// TODO: reset state
			// TODO: buildErrors shouldn't reset/use nextId/lastId, they're more like a global cache
			return
		}
	}

	// If we've collected output, but there were no tests or benchmarks then
	// either there were no tests, or there was some other non-build error.
	if len(b.output) > 0 && len(b.tests) == 0 && len(b.benchmarks) == 0 {
		pkg := Package{
			Name:     name,
			Duration: duration,
		}
		if parseResult(result) == Fail {
			pkg.RunError = Error{
				Name:   name,
				Output: b.output,
			}
		}
		b.packages = append(b.packages, pkg)

		// TODO: reset state
		b.output = nil
		return
	}

	// Collect tests and benchmarks for this package, maintaining insertion order.
	var tests []Test
	var benchmarks []Benchmark
	for id := 1; id < b.nextId; id++ {
		if t, ok := b.tests[id]; ok {
			tests = append(tests, t)
		}
		if bm, ok := b.benchmarks[id]; ok {
			benchmarks = append(benchmarks, bm)
		}
	}

	b.packages = append(b.packages, Package{
		Name:       name,
		Duration:   duration,
		Coverage:   b.coverage,
		Output:     b.output,
		Tests:      tests,
		Benchmarks: benchmarks,
	})

	b.nextId = 1
	b.lastId = 0
	b.output = nil
	b.coverage = 0
	b.tests = make(map[int]Test)
	b.benchmarks = make(map[int]Benchmark)
}

func (b *ReportBuilder) Coverage(pct float64, packages []string) {
	b.coverage = pct
}

func (b *ReportBuilder) AppendOutput(line string) {
	if b.lastId <= 0 {
		b.output = append(b.output, line)
		return
	}

	if t, ok := b.tests[b.lastId]; ok {
		t.Output = append(t.Output, line)
		b.tests[b.lastId] = t
	} else if bm, ok := b.benchmarks[b.lastId]; ok {
		bm.Output = append(bm.Output, line)
		b.benchmarks[b.lastId] = bm
	} else if be, ok := b.buildErrors[b.lastId]; ok {
		be.Output = append(be.Output, line)
		b.buildErrors[b.lastId] = be
	} else {
		fmt.Printf("DEBUG output else\n")
		b.output = append(b.output, line)
	}
}

func (b *ReportBuilder) findTest(name string) int {
	// check if this test was lastId
	if t, ok := b.tests[b.lastId]; ok && t.Name == name {
		return b.lastId
	}
	for id := len(b.tests); id >= 0; id-- {
		if b.tests[id].Name == name {
			return id
		}
	}
	return -1
}

func (b *ReportBuilder) findBenchmark(name string) int {
	// check if this benchmark was lastId
	if bm, ok := b.benchmarks[b.lastId]; ok && bm.Name == name {
		return b.lastId
	}
	for id := len(b.benchmarks); id >= 0; id-- {
		if b.benchmarks[id].Name == name {
			return id
		}
	}
	return -1
}

func parseResult(r string) Result {
	switch r {
	case "PASS":
		return Pass
	case "FAIL":
		return Fail
	case "SKIP":
		return Skip
	default:
		fmt.Printf("unknown result: %q\n", r)
		return Unknown
	}
}
<commit_msg>gtr: Handle failed summary when no tests failed<commit_after>package gtr

import (
	"fmt"
	"time"
)

// ReportBuilder builds Reports.
type ReportBuilder struct {
	packages    []Package
	tests       map[int]Test
	benchmarks  map[int]Benchmark
	buildErrors map[int]Error
	runErrors   map[int]Error

	// state
	nextId   int // next free id
	lastId   int // last test id // TODO: stack?
	output   []string
	coverage float64

	// defaults
	packageName string
}

// TODO: move gtr.FromEvents to ReportBuilder so we could have report builders for different parsers.
func NewReportBuilder(packageName string) *ReportBuilder {
	return &ReportBuilder{
		tests:       make(map[int]Test),
		benchmarks:  make(map[int]Benchmark),
		buildErrors: make(map[int]Error),
		runErrors:   make(map[int]Error),
		nextId:      1,
		packageName: packageName,
	}
}

func (b *ReportBuilder) newId() int {
	id := b.nextId
	b.lastId = id
	b.nextId += 1
	return id
}

func (b *ReportBuilder) flush() {
	// Create package in case we have tests or benchmarks that didn't get a
	// summary.
	if len(b.tests) > 0 || len(b.benchmarks) > 0 {
		b.CreatePackage(b.packageName, "", 0, "")
	}
}

func (b *ReportBuilder) Build() Report {
	b.flush()
	return Report{Packages: b.packages}
}

func (b *ReportBuilder) CreateTest(name string) {
	b.tests[b.newId()] = Test{Name: name}
}

func (b *ReportBuilder) PauseTest(name string) {
	b.lastId = 0
}

func (b *ReportBuilder) ContinueTest(name string) {
	b.lastId = b.findTest(name)
}

func (b *ReportBuilder) EndTest(name, result string, duration time.Duration) {
	id := b.findTest(name)
	b.lastId = id

	t := b.tests[id]
	t.Result = parseResult(result)
	t.Duration = duration
	b.tests[id] = t
}

func (b *ReportBuilder) End() {
	b.lastId = 0
}

func (b *ReportBuilder) Benchmark(name string, iterations int64, nsPerOp, mbPerSec float64, bytesPerOp, allocsPerOp int64) {
	b.benchmarks[b.newId()] = Benchmark{
		Name:        name,
		Result:      Pass,
		Iterations:  iterations,
		NsPerOp:     nsPerOp,
		MBPerSec:    mbPerSec,
		BytesPerOp:  bytesPerOp,
		AllocsPerOp: allocsPerOp,
	}
}

func (b *ReportBuilder) CreateBuildError(packageName string) {
	b.buildErrors[b.newId()] = Error{Name: packageName}
}

func (b *ReportBuilder) CreatePackage(name, result string, duration time.Duration, data string) {
	pkg := Package{
		Name:     name,
		Duration: duration,
	}

	// Build errors are treated somewhat differently. Rather than having a
	// single package with all build errors collected so far, we only care
	// about the build errors for this particular package.
	for id, buildErr := range b.buildErrors {
		if buildErr.Name == name {
			if len(b.tests) > 0 || len(b.benchmarks) > 0 {
				panic("unexpected tests and/or benchmarks found in build error package")
			}
			buildErr.Duration = duration
			buildErr.Cause = data
			pkg.BuildError = buildErr
			b.packages = append(b.packages, pkg)

			delete(b.buildErrors, id)
			// TODO: reset state
			// TODO: buildErrors shouldn't reset/use nextId/lastId, they're more like a global cache
			return
		}
	}

	// If we've collected output, but there were no tests or benchmarks then
	// either there were no tests, or there was some other non-build error.
	if len(b.output) > 0 && len(b.tests) == 0 && len(b.benchmarks) == 0 {
		if parseResult(result) == Fail {
			pkg.RunError = Error{
				Name:   name,
				Output: b.output,
			}
		}
		b.packages = append(b.packages, pkg)

		// TODO: reset state
		b.output = nil
		return
	}

	// If the summary result says we failed, but there were no failing tests
	// then something else must have failed.
	if parseResult(result) == Fail && (len(b.tests) > 0 || len(b.benchmarks) > 0) && !b.containsFailingTest() {
		pkg.RunError = Error{
			Name:   name,
			Output: b.output,
		}
		b.output = nil
	}

	// Collect tests and benchmarks for this package, maintaining insertion order.
	var tests []Test
	var benchmarks []Benchmark
	for id := 1; id < b.nextId; id++ {
		if t, ok := b.tests[id]; ok {
			tests = append(tests, t)
		}
		if bm, ok := b.benchmarks[id]; ok {
			benchmarks = append(benchmarks, bm)
		}
	}

	pkg.Coverage = b.coverage
	pkg.Output = b.output
	pkg.Tests = tests
	pkg.Benchmarks = benchmarks
	b.packages = append(b.packages, pkg)

	b.nextId = 1
	b.lastId = 0
	b.output = nil
	b.coverage = 0
	b.tests = make(map[int]Test)
	b.benchmarks = make(map[int]Benchmark)
}

func (b *ReportBuilder) Coverage(pct float64, packages []string) {
	b.coverage = pct
}

func (b *ReportBuilder) AppendOutput(line string) {
	if b.lastId <= 0 {
		b.output = append(b.output, line)
		return
	}

	if t, ok := b.tests[b.lastId]; ok {
		t.Output = append(t.Output, line)
		b.tests[b.lastId] = t
	} else if bm, ok := b.benchmarks[b.lastId]; ok {
		bm.Output = append(bm.Output, line)
		b.benchmarks[b.lastId] = bm
	} else if be, ok := b.buildErrors[b.lastId]; ok {
		be.Output = append(be.Output, line)
		b.buildErrors[b.lastId] = be
	} else {
		fmt.Printf("DEBUG output else\n")
		b.output = append(b.output, line)
	}
}

func (b *ReportBuilder) findTest(name string) int {
	// check if this test was lastId
	if t, ok := b.tests[b.lastId]; ok && t.Name == name {
		return b.lastId
	}
	for id := len(b.tests); id >= 0; id-- {
		if b.tests[id].Name == name {
			return id
		}
	}
	return -1
}

func (b *ReportBuilder) findBenchmark(name string) int {
	// check if this benchmark was lastId
	if bm, ok := b.benchmarks[b.lastId]; ok && bm.Name == name {
		return b.lastId
	}
	for id := len(b.benchmarks); id >= 0; id-- {
		if b.benchmarks[id].Name == name {
			return id
		}
	}
	return -1
}

func (b *ReportBuilder) containsFailingTest() bool {
	for _, test := range b.tests {
		if test.Result == Fail {
			return true
		}
	}
	return false
}

func parseResult(r string) Result {
	switch r {
	case "PASS":
		return Pass
	case "FAIL":
		return Fail
	case "SKIP":
		return Skip
	default:
		fmt.Printf("unknown result: %q\n", r)
		return Unknown
	}
}
<|endoftext|>


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

package ioutil

import (
	"io"
	"os"
)

// WriteAndSyncFile behaviors just like ioutil.WriteFile in standard library
// but calls Sync before closing the file. WriteAndSyncFile guarantees the data
// is synced if there is no error returned.
func WriteAndSyncFile(filename string, data []byte, perm os.FileMode) error {
	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
	if err != nil {
		return err
	}
	n, err := f.Write(data)
	if err == nil && n < len(data) {
		err = io.ErrShortWrite
	}
	if err == nil {
		err = f.Sync()
	}
	if err1 := f.Close(); err == nil {
		err = err1
	}
	return err
}
<commit_msg>pkg: fix tiny docstring typo in ioutil<commit_after>// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ioutil

import (
	"io"
	"os"
)

// WriteAndSyncFile behaves just like ioutil.WriteFile in the standard library,
// but calls Sync before closing the file. WriteAndSyncFile guarantees the data
// is synced if there is no error returned.
func WriteAndSyncFile(filename string, data []byte, perm os.FileMode) error {
	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
	if err != nil {
		return err
	}
	n, err := f.Write(data)
	if err == nil && n < len(data) {
		err = io.ErrShortWrite
	}
	if err == nil {
		err = f.Sync()
	}
	if err1 := f.Close(); err == nil {
		err = err1
	}
	return err
}
<|endoftext|>


<commit_before>package model

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/ViBiOh/flags"
	"github.com/ViBiOh/httputils/v4/pkg/concurrent"
	"github.com/ViBiOh/httputils/v4/pkg/cron"
	"github.com/ViBiOh/httputils/v4/pkg/db"
	"github.com/ViBiOh/httputils/v4/pkg/httperror"
	"github.com/ViBiOh/httputils/v4/pkg/httpjson"
	"github.com/ViBiOh/httputils/v4/pkg/logger"
	"github.com/ViBiOh/httputils/v4/pkg/tracer"
	"go.opentelemetry.io/otel/trace"
)

const (
	listPrefix = "/list"
)

// Config of package
type Config struct {
	infos *string
}

// App of package
type App struct {
	tracer   trace.Tracer
	db       db.App
	fundsURL string
	fundsMap sync.Map
}

// Flags adds flags for configuring package
func Flags(fs *flag.FlagSet, prefix string) Config {
	return Config{
		infos: flags.New(prefix, "funds", "Infos").Default("", nil).Label("Informations URL").ToString(fs),
	}
}

// New creates new App from Config
func New(config Config, db db.App, tracerApp tracer.App) *App {
	return &App{
		fundsURL: strings.TrimSpace(*config.infos),
		fundsMap: sync.Map{},

		db:     db,
		tracer: tracerApp.GetTracer("model"),
	}
}

// Start worker
func (a *App) Start(done <-chan struct{}) {
	cron.New().Each(time.Hour*8).Now().OnError(func(err error) {
		logger.Error("%s", err)
	}).Start(a.refresh, done)
}

func (a *App) refresh(ctx context.Context) error {
	if a.fundsURL == "" {
		return nil
	}

	if a.tracer != nil {
		var span trace.Span
		ctx, span = a.tracer.Start(ctx, "refresh")
		defer span.End()
	}

	a.refreshData(ctx)

	if a.db.Enabled() {
		if err := a.saveData(ctx); err != nil {
			return err
		}
	}

	return nil
}

func (a *App) refreshData(ctx context.Context) {
	wg := concurrent.NewLimited(4)

	for _, fundID := range fundsIds {
		func(fundID []byte) {
			wg.Go(func() {
				if output, err := fetchFund(ctx, a.fundsURL, fundID); err != nil {
					logger.Error("%s", err)
				} else {
					a.fundsMap.Store(output.ID, output)
				}

				time.Sleep(10 * time.Second)
			})
		}(fundID)
	}

	wg.Wait()
}

func (a *App) saveData(ctx context.Context) (err error) {
	a.fundsMap.Range(func(_ interface{}, value interface{}) bool {
		fund := value.(Fund)
		err := a.db.DoAtomic(ctx, func(ctx context.Context) error {
			return a.saveFund(ctx, &fund)
		})

		return err == nil
	})

	return
}

// Health check health
func (a *App) Health() error {
	if len(a.ListFunds(nil)) == 0 {
		return errors.New("no funds fetched")
	}

	return nil
}

// ListFunds return content of funds' map
func (a *App) ListFunds(alerts []Alert) []Fund {
	funds := make([]Fund, 0, len(fundsIds))

	a.fundsMap.Range(func(_ interface{}, value interface{}) bool {
		fund := value.(Fund)
		for _, alert := range alerts {
			fundAlert := alert

			if fund.Isin == alert.Isin {
				fund.Alert = &fundAlert
			}
		}

		funds = append(funds, fund)
		return true
	})

	return funds
}

func (a *App) listHandler(w http.ResponseWriter, r *http.Request) {
	alerts, err := a.GetIsinAlert(r.Context())
	if err != nil {
		httperror.InternalServerError(w, fmt.Errorf("unable to retrieve alerts: %w", err))
		return
	}

	httpjson.WriteArray(w, http.StatusOK, a.ListFunds(alerts))
}

// Handler for model request. Should be use with net/http
func (a *App) Handler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodOptions {
			if _, err := w.Write(nil); err != nil {
				httperror.InternalServerError(w, err)
			}
			return
		}
		if r.Method != http.MethodGet {
			w.WriteHeader(http.StatusMethodNotAllowed)
		}

		if strings.HasPrefix(r.URL.Path, listPrefix) {
			a.listHandler(w, r)
		} else if r.URL.Path == "/ready" {
			if a.Health() == nil {
				w.WriteHeader(http.StatusOK)
			} else {
				w.WriteHeader(http.StatusTeapot)
			}
			return
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	})
}
<commit_msg>fix(cron): Adding tracer when possible<commit_after>package model

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/ViBiOh/flags"
	"github.com/ViBiOh/httputils/v4/pkg/concurrent"
	"github.com/ViBiOh/httputils/v4/pkg/cron"
	"github.com/ViBiOh/httputils/v4/pkg/db"
	"github.com/ViBiOh/httputils/v4/pkg/httperror"
	"github.com/ViBiOh/httputils/v4/pkg/httpjson"
	"github.com/ViBiOh/httputils/v4/pkg/logger"
	"github.com/ViBiOh/httputils/v4/pkg/tracer"
	"go.opentelemetry.io/otel/trace"
)

const (
	listPrefix = "/list"
)

// Config of package
type Config struct {
	infos *string
}

// App of package
type App struct {
	tracer   trace.Tracer
	db       db.App
	fundsURL string
	fundsMap sync.Map
}

// Flags adds flags for configuring package
func Flags(fs *flag.FlagSet, prefix string) Config {
	return Config{
		infos: flags.New(prefix, "funds", "Infos").Default("", nil).Label("Informations URL").ToString(fs),
	}
}

// New creates new App from Config
func New(config Config, db db.App, tracerApp tracer.App) *App {
	return &App{
		fundsURL: strings.TrimSpace(*config.infos),
		fundsMap: sync.Map{},

		db:     db,
		tracer: tracerApp.GetTracer("model"),
	}
}

// Start worker
func (a *App) Start(done <-chan struct{}) {
	cron.New().Each(time.Hour*8).Now().WithTracer(a.tracer).OnError(func(err error) {
		logger.Error("%s", err)
	}).Start(a.refresh, done)
}

func (a *App) refresh(ctx context.Context) error {
	if a.fundsURL == "" {
		return nil
	}

	if a.tracer != nil {
		var span trace.Span
		ctx, span = a.tracer.Start(ctx, "refresh")
		defer span.End()
	}

	a.refreshData(ctx)

	if a.db.Enabled() {
		if err := a.saveData(ctx); err != nil {
			return err
		}
	}

	return nil
}

func (a *App) refreshData(ctx context.Context) {
	wg := concurrent.NewLimited(4)

	for _, fundID := range fundsIds {
		func(fundID []byte) {
			wg.Go(func() {
				if output, err := fetchFund(ctx, a.fundsURL, fundID); err != nil {
					logger.Error("%s", err)
				} else {
					a.fundsMap.Store(output.ID, output)
				}

				time.Sleep(10 * time.Second)
			})
		}(fundID)
	}

	wg.Wait()
}

func (a *App) saveData(ctx context.Context) (err error) {
	a.fundsMap.Range(func(_ interface{}, value interface{}) bool {
		fund := value.(Fund)
		err := a.db.DoAtomic(ctx, func(ctx context.Context) error {
			return a.saveFund(ctx, &fund)
		})

		return err == nil
	})

	return
}

// Health check health
func (a *App) Health() error {
	if len(a.ListFunds(nil)) == 0 {
		return errors.New("no funds fetched")
	}

	return nil
}

// ListFunds return content of funds' map
func (a *App) ListFunds(alerts []Alert) []Fund {
	funds := make([]Fund, 0, len(fundsIds))

	a.fundsMap.Range(func(_ interface{}, value interface{}) bool {
		fund := value.(Fund)
		for _, alert := range alerts {
			fundAlert := alert

			if fund.Isin == alert.Isin {
				fund.Alert = &fundAlert
			}
		}

		funds = append(funds, fund)
		return true
	})

	return funds
}

func (a *App) listHandler(w http.ResponseWriter, r *http.Request) {
	alerts, err := a.GetIsinAlert(r.Context())
	if err != nil {
		httperror.InternalServerError(w, fmt.Errorf("unable to retrieve alerts: %w", err))
		return
	}

	httpjson.WriteArray(w, http.StatusOK, a.ListFunds(alerts))
}

// Handler for model request. Should be use with net/http
func (a *App) Handler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodOptions {
			if _, err := w.Write(nil); err != nil {
				httperror.InternalServerError(w, err)
			}
			return
		}
		if r.Method != http.MethodGet {
			w.WriteHeader(http.StatusMethodNotAllowed)
		}

		if strings.HasPrefix(r.URL.Path, listPrefix) {
			a.listHandler(w, r)
		} else if r.URL.Path == "/ready" {
			if a.Health() == nil {
				w.WriteHeader(http.StatusOK)
			} else {
				w.WriteHeader(http.StatusTeapot)
			}
			return
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	})
}
<|endoftext|>