From dd84b9d64fb98746a230cd24233ff50a562c39c9 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 28 Apr 2023 01:36:44 +0800 Subject: --- cli/internal/util/backends.go | 30 ++++++ cli/internal/util/browser/open.go | 37 +++++++ cli/internal/util/closer.go | 15 +++ cli/internal/util/cmd.go | 24 +++++ cli/internal/util/filter/filter.go | 133 +++++++++++++++++++++++++ cli/internal/util/filter/filter_test.go | 116 ++++++++++++++++++++++ cli/internal/util/graph.go | 35 +++++++ cli/internal/util/modulo.go | 13 +++ cli/internal/util/parse_concurrency.go | 39 ++++++++ cli/internal/util/parse_concurrency_test.go | 79 +++++++++++++++ cli/internal/util/printf.go | 63 ++++++++++++ cli/internal/util/run_opts.go | 53 ++++++++++ cli/internal/util/semaphore.go | 43 ++++++++ cli/internal/util/set.go | 147 +++++++++++++++++++++++++++ cli/internal/util/set_test.go | 149 ++++++++++++++++++++++++++++ cli/internal/util/status.go | 47 +++++++++ cli/internal/util/task_id.go | 66 ++++++++++++ cli/internal/util/task_output_mode.go | 100 +++++++++++++++++++ 18 files changed, 1189 insertions(+) create mode 100644 cli/internal/util/backends.go create mode 100644 cli/internal/util/browser/open.go create mode 100644 cli/internal/util/closer.go create mode 100644 cli/internal/util/cmd.go create mode 100644 cli/internal/util/filter/filter.go create mode 100644 cli/internal/util/filter/filter_test.go create mode 100644 cli/internal/util/graph.go create mode 100644 cli/internal/util/modulo.go create mode 100644 cli/internal/util/parse_concurrency.go create mode 100644 cli/internal/util/parse_concurrency_test.go create mode 100644 cli/internal/util/printf.go create mode 100644 cli/internal/util/run_opts.go create mode 100644 cli/internal/util/semaphore.go create mode 100644 cli/internal/util/set.go create mode 100644 cli/internal/util/set_test.go create mode 100644 cli/internal/util/status.go create mode 100644 cli/internal/util/task_id.go create mode 100644 cli/internal/util/task_output_mode.go (limited to 'cli/internal/util') diff --git a/cli/internal/util/backends.go b/cli/internal/util/backends.go new file mode 100644 index 0000000..66941ad --- /dev/null +++ b/cli/internal/util/backends.go @@ -0,0 +1,30 @@ +package util + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/vercel/turbo/cli/internal/yaml" +) + +// YarnRC Represents contents of .yarnrc.yml +type YarnRC struct { + NodeLinker string `yaml:"nodeLinker"` +} + +// IsNMLinker Checks that Yarn is set to use the node-modules linker style +func IsNMLinker(cwd string) (bool, error) { + yarnRC := &YarnRC{} + + bytes, err := ioutil.ReadFile(filepath.Join(cwd, ".yarnrc.yml")) + if err != nil { + return false, fmt.Errorf(".yarnrc.yml: %w", err) + } + + if yaml.Unmarshal(bytes, yarnRC) != nil { + return false, fmt.Errorf(".yarnrc.yml: %w", err) + } + + return yarnRC.NodeLinker == "node-modules", nil +} diff --git a/cli/internal/util/browser/open.go b/cli/internal/util/browser/open.go new file mode 100644 index 0000000..a6171e9 --- /dev/null +++ b/cli/internal/util/browser/open.go @@ -0,0 +1,37 @@ +package browser + +import ( + "fmt" + "os/exec" + "runtime" +) + +// OpenBrowser attempts to interactively open a browser window at the given URL +func OpenBrowser(url string) error { + var err error + + switch runtime.GOOS { + case "linux": + if posixBinExists("wslview") { + err = exec.Command("wslview", url).Start() + } else { + err = exec.Command("xdg-open", url).Start() + } + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + if err != nil { + return err + } + return nil +} + +func posixBinExists(bin string) bool { + err := exec.Command("which", bin).Run() + // we mostly don't care what the error is, it suggests the binary is not usable + return err == nil +} diff --git a/cli/internal/util/closer.go b/cli/internal/util/closer.go new file mode 100644 index 0000000..996760b --- /dev/null +++ b/cli/internal/util/closer.go @@ -0,0 +1,15 @@ +package util + +// CloseAndIgnoreError is a utility to tell our linter that we explicitly deem it okay +// to not check a particular error on closing of a resource. +// +// We use `errcheck` as a linter, which is super-opinionated about checking errors, +// even in places where we don't necessarily care to check the error. +// +// `golangci-lint` has a default ignore list for this lint problem (EXC0001) which +// can be used to sidestep this problem but it's possibly a little too-heavy-handed +// in exclusion. At the expense of discoverability, this utility function forces +// opt-in to ignoring errors on closing of things that can be `Close`d. +func CloseAndIgnoreError(closer interface{ Close() error }) { + _ = closer.Close() +} diff --git a/cli/internal/util/cmd.go b/cli/internal/util/cmd.go new file mode 100644 index 0000000..ae79aa0 --- /dev/null +++ b/cli/internal/util/cmd.go @@ -0,0 +1,24 @@ +package util + +import ( + "bytes" + + "github.com/spf13/cobra" +) + +// ExitCodeError is a specific error that is returned by the command to specify the exit code +type ExitCodeError struct { + ExitCode int +} + +func (e *ExitCodeError) Error() string { return "exit code error" } + +// HelpForCobraCmd returns the help string for a given command +// Note that this overwrites the output for the command +func HelpForCobraCmd(cmd *cobra.Command) string { + f := cmd.HelpFunc() + buf := bytes.NewBufferString("") + cmd.SetOut(buf) + f(cmd, []string{}) + return buf.String() +} diff --git a/cli/internal/util/filter/filter.go b/cli/internal/util/filter/filter.go new file mode 100644 index 0000000..fbc475d --- /dev/null +++ b/cli/internal/util/filter/filter.go @@ -0,0 +1,133 @@ +// Copyright (c) 2015-2020 InfluxData Inc. MIT License (MIT) +// https://github.com/influxdata/telegraf +package filter + +import ( + "strings" + + "github.com/gobwas/glob" +) + +type Filter interface { + Match(string) bool +} + +// Compile takes a list of string filters and returns a Filter interface +// for matching a given string against the filter list. The filter list +// supports glob matching too, ie: +// +// f, _ := Compile([]string{"cpu", "mem", "net*"}) +// f.Match("cpu") // true +// f.Match("network") // true +// f.Match("memory") // false +func Compile(filters []string) (Filter, error) { + // return if there is nothing to compile + if len(filters) == 0 { + return nil, nil + } + + // check if we can compile a non-glob filter + noGlob := true + for _, filter := range filters { + if hasMeta(filter) { + noGlob = false + break + } + } + + switch { + case noGlob: + // return non-globbing filter if not needed. + return compileFilterNoGlob(filters), nil + case len(filters) == 1: + return glob.Compile(filters[0]) + default: + return glob.Compile("{" + strings.Join(filters, ",") + "}") + } +} + +// hasMeta reports whether path contains any magic glob characters. +func hasMeta(s string) bool { + return strings.ContainsAny(s, "*?[") +} + +type filter struct { + m map[string]struct{} +} + +func (f *filter) Match(s string) bool { + _, ok := f.m[s] + return ok +} + +type filtersingle struct { + s string +} + +func (f *filtersingle) Match(s string) bool { + return f.s == s +} + +func compileFilterNoGlob(filters []string) Filter { + if len(filters) == 1 { + return &filtersingle{s: filters[0]} + } + out := filter{m: make(map[string]struct{})} + for _, filter := range filters { + out.m[filter] = struct{}{} + } + return &out +} + +type IncludeExcludeFilter struct { + include Filter + exclude Filter + includeDefault bool + excludeDefault bool +} + +func NewIncludeExcludeFilter( + include []string, + exclude []string, +) (Filter, error) { + return NewIncludeExcludeFilterDefaults(include, exclude, true, false) +} + +func NewIncludeExcludeFilterDefaults( + include []string, + exclude []string, + includeDefault bool, + excludeDefault bool, +) (Filter, error) { + in, err := Compile(include) + if err != nil { + return nil, err + } + + ex, err := Compile(exclude) + if err != nil { + return nil, err + } + + return &IncludeExcludeFilter{in, ex, includeDefault, excludeDefault}, nil +} + +func (f *IncludeExcludeFilter) Match(s string) bool { + if f.include != nil { + if !f.include.Match(s) { + return false + } + } else if !f.includeDefault { + return false + } + + if f.exclude != nil { + if f.exclude.Match(s) { + return false + } + } else if f.excludeDefault { + return false + } + + return true +} diff --git a/cli/internal/util/filter/filter_test.go b/cli/internal/util/filter/filter_test.go new file mode 100644 index 0000000..727a4b6 --- /dev/null +++ b/cli/internal/util/filter/filter_test.go @@ -0,0 +1,116 @@ +// Copyright (c) 2015-2020 InfluxData Inc. MIT License (MIT) +// https://github.com/influxdata/telegraf +package filter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCompile(t *testing.T) { + f, err := Compile([]string{}) + assert.NoError(t, err) + assert.Nil(t, f) + + f, err = Compile([]string{"cpu"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.False(t, f.Match("cpu0")) + assert.False(t, f.Match("mem")) + + f, err = Compile([]string{"cpu*"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.True(t, f.Match("cpu0")) + assert.False(t, f.Match("mem")) + + f, err = Compile([]string{"cpu", "mem"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.False(t, f.Match("cpu0")) + assert.True(t, f.Match("mem")) + + f, err = Compile([]string{"cpu", "mem", "net*"}) + assert.NoError(t, err) + assert.True(t, f.Match("cpu")) + assert.False(t, f.Match("cpu0")) + assert.True(t, f.Match("mem")) + assert.True(t, f.Match("network")) +} + +func TestIncludeExclude(t *testing.T) { + tags := []string{} + labels := []string{"best", "com_influxdata", "timeseries", "com_influxdata_telegraf", "ever"} + + filter, err := NewIncludeExcludeFilter([]string{}, []string{"com_influx*"}) + if err != nil { + t.Fatalf("Failed to create include/exclude filter - %v", err) + } + + for i := range labels { + if filter.Match(labels[i]) { + tags = append(tags, labels[i]) + } + } + + assert.Equal(t, []string{"best", "timeseries", "ever"}, tags) +} + +var benchbool bool + +func BenchmarkFilterSingleNoGlobFalse(b *testing.B) { + f, _ := Compile([]string{"cpu"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("network") + } + benchbool = tmp +} + +func BenchmarkFilterSingleNoGlobTrue(b *testing.B) { + f, _ := Compile([]string{"cpu"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("cpu") + } + benchbool = tmp +} + +func BenchmarkFilter(b *testing.B) { + f, _ := Compile([]string{"cpu", "mem", "net*"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("network") + } + benchbool = tmp +} + +func BenchmarkFilterNoGlob(b *testing.B) { + f, _ := Compile([]string{"cpu", "mem", "net"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("net") + } + benchbool = tmp +} + +func BenchmarkFilter2(b *testing.B) { + f, _ := Compile([]string{"aa", "bb", "c", "ad", "ar", "at", "aq", + "aw", "az", "axxx", "ab", "cpu", "mem", "net*"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("network") + } + benchbool = tmp +} + +func BenchmarkFilter2NoGlob(b *testing.B) { + f, _ := Compile([]string{"aa", "bb", "c", "ad", "ar", "at", "aq", + "aw", "az", "axxx", "ab", "cpu", "mem", "net"}) + var tmp bool + for n := 0; n < b.N; n++ { + tmp = f.Match("net") + } + benchbool = tmp +} diff --git a/cli/internal/util/graph.go b/cli/internal/util/graph.go new file mode 100644 index 0000000..89de18c --- /dev/null +++ b/cli/internal/util/graph.go @@ -0,0 +1,35 @@ +package util + +import ( + "fmt" + "strings" + + "github.com/pyr-sh/dag" +) + +// ValidateGraph checks that a given DAG has no cycles and no self-referential edges. +// We differ from the underlying DAG Validate method in that we allow multiple roots. +func ValidateGraph(graph *dag.AcyclicGraph) error { + // We use Cycles instead of Validate because + // our DAG has multiple roots (entrypoints). + // Validate mandates that there is only a single root node. + cycles := graph.Cycles() + if len(cycles) > 0 { + cycleLines := make([]string, len(cycles)) + for i, cycle := range cycles { + vertices := make([]string, len(cycle)) + for j, vertex := range cycle { + vertices[j] = vertex.(string) + } + cycleLines[i] = "\t" + strings.Join(vertices, ",") + } + return fmt.Errorf("cyclic dependency detected:\n%s", strings.Join(cycleLines, "\n")) + } + + for _, e := range graph.Edges() { + if e.Source() == e.Target() { + return fmt.Errorf("%s depends on itself", e.Source()) + } + } + return nil +} diff --git a/cli/internal/util/modulo.go b/cli/internal/util/modulo.go new file mode 100644 index 0000000..ec2957a --- /dev/null +++ b/cli/internal/util/modulo.go @@ -0,0 +1,13 @@ +package util + +// PostitiveMod returns a modulo operator like JavaScripts +func PositiveMod(x, d int) int { + x = x % d + if x >= 0 { + return x + } + if d < 0 { + return x - d + } + return x + d +} diff --git a/cli/internal/util/parse_concurrency.go b/cli/internal/util/parse_concurrency.go new file mode 100644 index 0000000..6917600 --- /dev/null +++ b/cli/internal/util/parse_concurrency.go @@ -0,0 +1,39 @@ +package util + +import ( + "fmt" + "math" + "runtime" + "strconv" + "strings" +) + +var ( + // alias so we can mock in tests + runtimeNumCPU = runtime.NumCPU + // positive values check for +Inf + _positiveInfinity = 1 +) + +// ParseConcurrency parses a concurrency value, which can be a number (e.g. 2) or a percentage (e.g. 50%). +func ParseConcurrency(concurrencyRaw string) (int, error) { + if strings.HasSuffix(concurrencyRaw, "%") { + if percent, err := strconv.ParseFloat(concurrencyRaw[:len(concurrencyRaw)-1], 64); err != nil { + return 0, fmt.Errorf("invalid value for --concurrency CLI flag. This should be a number --concurrency=4 or percentage of CPU cores --concurrency=50%% : %w", err) + } else { + if percent > 0 && !math.IsInf(percent, _positiveInfinity) { + return int(math.Max(1, float64(runtimeNumCPU())*percent/100)), nil + } else { + return 0, fmt.Errorf("invalid percentage value for --concurrency CLI flag. This should be a percentage of CPU cores, between 1%% and 100%% : %w", err) + } + } + } else if i, err := strconv.Atoi(concurrencyRaw); err != nil { + return 0, fmt.Errorf("invalid value for --concurrency CLI flag. This should be a positive integer greater than or equal to 1: %w", err) + } else { + if i >= 1 { + return i, nil + } else { + return 0, fmt.Errorf("invalid value %v for --concurrency CLI flag. This should be a positive integer greater than or equal to 1", i) + } + } +} diff --git a/cli/internal/util/parse_concurrency_test.go b/cli/internal/util/parse_concurrency_test.go new file mode 100644 index 0000000..b732724 --- /dev/null +++ b/cli/internal/util/parse_concurrency_test.go @@ -0,0 +1,79 @@ +package util + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseConcurrency(t *testing.T) { + cases := []struct { + Input string + Expected int + }{ + { + "12", + 12, + }, + { + "200%", + 20, + }, + { + "100%", + 10, + }, + { + "50%", + 5, + }, + { + "25%", + 2, + }, + { + "1%", + 1, + }, + { + "0644", // we parse in base 10 + 644, + }, + } + + // mock runtime.NumCPU() to 10 + runtimeNumCPU = func() int { + return 10 + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d) '%s' should be parsed at '%d'", i, tc.Input, tc.Expected), func(t *testing.T) { + if result, err := ParseConcurrency(tc.Input); err != nil { + t.Fatalf("invalid parse: %#v", err) + } else { + assert.EqualValues(t, tc.Expected, result) + } + }) + } +} + +func TestInvalidPercents(t *testing.T) { + inputs := []string{ + "asdf", + "-1", + "-l%", + "infinity%", + "-infinity%", + "nan%", + "0b01", + "0o644", + "0xFF", + } + for _, tc := range inputs { + t.Run(tc, func(t *testing.T) { + val, err := ParseConcurrency(tc) + assert.Error(t, err, "input %v got %v", tc, val) + }) + } +} diff --git a/cli/internal/util/printf.go b/cli/internal/util/printf.go new file mode 100644 index 0000000..9cd6dce --- /dev/null +++ b/cli/internal/util/printf.go @@ -0,0 +1,63 @@ +// Copyright Thought Machine, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package util + +import ( + "fmt" + "io" + "os" + + "github.com/vercel/turbo/cli/internal/ui" +) + +// initPrintf sets up the replacements used by printf. +func InitPrintf() { + if !ui.IsTTY { + replacements = map[string]string{} + } +} + +// printf is used throughout this package to print something to stderr with some +// replacements for pseudo-shell variables for ANSI formatting codes. +func Sprintf(format string, args ...interface{}) string { + return os.Expand(fmt.Sprintf(format, args...), replace) +} + +func Printf(format string, args ...interface{}) { + fmt.Fprint(os.Stderr, os.Expand(fmt.Sprintf(format, args...), replace)) +} + +func Fprintf(writer io.Writer, format string, args ...interface{}) { + fmt.Fprint(writer, os.Expand(fmt.Sprintf(format, args...), replace)) +} + +func replace(s string) string { + return replacements[s] +} + +// These are the standard set of replacements we use. +var replacements = map[string]string{ + "BOLD": "\x1b[1m", + "BOLD_GREY": "\x1b[30;1m", + "BOLD_RED": "\x1b[31;1m", + "BOLD_GREEN": "\x1b[32;1m", + "BOLD_YELLOW": "\x1b[33;1m", + "BOLD_BLUE": "\x1b[34;1m", + "BOLD_MAGENTA": "\x1b[35;1m", + "BOLD_CYAN": "\x1b[36;1m", + "BOLD_WHITE": "\x1b[37;1m", + "UNDERLINE": "\x1b[4m", + "GREY": "\x1b[2m", + "RED": "\x1b[31m", + "GREEN": "\x1b[32m", + "YELLOW": "\x1b[33m", + "BLUE": "\x1b[34m", + "MAGENTA": "\x1b[35m", + "CYAN": "\x1b[36m", + "WHITE": "\x1b[37m", + "WHITE_ON_RED": "\x1b[37;41;1m", + "RED_NO_BG": "\x1b[31;49;1m", + "RESET": "\x1b[0m", + "ERASE_AFTER": "\x1b[K", + "CLEAR_END": "\x1b[0J", +} diff --git a/cli/internal/util/run_opts.go b/cli/internal/util/run_opts.go new file mode 100644 index 0000000..08676a0 --- /dev/null +++ b/cli/internal/util/run_opts.go @@ -0,0 +1,53 @@ +package util + +import "strings" + +// EnvMode specifies if we will be using strict env vars +type EnvMode string + +const ( + // Infer - infer environment variable constraints from turbo.json + Infer EnvMode = "Infer" + // Loose - environment variables are unconstrained + Loose EnvMode = "Loose" + // Strict - environment variables are limited + Strict EnvMode = "Strict" +) + +// MarshalText implements TextMarshaler for the struct. +func (s EnvMode) MarshalText() (text []byte, err error) { + return []byte(strings.ToLower(string(s))), nil +} + +// RunOpts holds the options that control the execution of a turbo run +type RunOpts struct { + // Force execution to be serially one-at-a-time + Concurrency int + // Whether to execute in parallel (defaults to false) + Parallel bool + + EnvMode EnvMode + // The filename to write a perf profile. + Profile string + // If true, continue task executions even if a task fails. + ContinueOnError bool + PassThroughArgs []string + // Restrict execution to only the listed task names. Default false + Only bool + // Dry run flags + DryRun bool + DryRunJSON bool + // Graph flags + GraphDot bool + GraphFile string + NoDaemon bool + SinglePackage bool + + // logPrefix controls whether we should print a prefix in task logs + LogPrefix string + + // Whether turbo should create a run summary + Summarize bool + + ExperimentalSpaceID string +} diff --git a/cli/internal/util/semaphore.go b/cli/internal/util/semaphore.go new file mode 100644 index 0000000..ef29df0 --- /dev/null +++ b/cli/internal/util/semaphore.go @@ -0,0 +1,43 @@ +package util + +// Semaphore is a wrapper around a channel to provide +// utility methods to clarify that we are treating the +// channel as a semaphore +type Semaphore chan struct{} + +// NewSemaphore creates a semaphore that allows up +// to a given limit of simultaneous acquisitions +func NewSemaphore(n int) Semaphore { + if n <= 0 { + panic("semaphore with limit <=0") + } + ch := make(chan struct{}, n) + return Semaphore(ch) +} + +// Acquire is used to acquire an available slot. +// Blocks until available. +func (s Semaphore) Acquire() { + s <- struct{}{} +} + +// TryAcquire is used to do a non-blocking acquire. +// Returns a bool indicating success +func (s Semaphore) TryAcquire() bool { + select { + case s <- struct{}{}: + return true + default: + return false + } +} + +// Release is used to return a slot. Acquire must +// be called as a pre-condition. +func (s Semaphore) Release() { + select { + case <-s: + default: + panic("release without an acquire") + } +} diff --git a/cli/internal/util/set.go b/cli/internal/util/set.go new file mode 100644 index 0000000..b6c5f86 --- /dev/null +++ b/cli/internal/util/set.go @@ -0,0 +1,147 @@ +package util + +// Set is a set data structure. +type Set map[interface{}]interface{} + +// SetFromStrings creates a Set containing the strings from the given slice +func SetFromStrings(sl []string) Set { + set := make(Set, len(sl)) + for _, item := range sl { + set.Add(item) + } + return set +} + +// Hashable is the interface used by set to get the hash code of a value. +// If this isn't given, then the value of the item being added to the set +// itself is used as the comparison value. +type Hashable interface { + Hashcode() interface{} +} + +// hashcode returns the hashcode used for set elements. +func hashcode(v interface{}) interface{} { + if h, ok := v.(Hashable); ok { + return h.Hashcode() + } + + return v +} + +// Add adds an item to the set +func (s Set) Add(v interface{}) { + s[hashcode(v)] = v +} + +// Delete removes an item from the set. +func (s Set) Delete(v interface{}) { + delete(s, hashcode(v)) +} + +// Includes returns true/false of whether a value is in the set. +func (s Set) Includes(v interface{}) bool { + _, ok := s[hashcode(v)] + return ok +} + +// Intersection computes the set intersection with other. +func (s Set) Intersection(other Set) Set { + result := make(Set) + if s == nil || other == nil { + return result + } + // Iteration over a smaller set has better performance. + if other.Len() < s.Len() { + s, other = other, s + } + for _, v := range s { + if other.Includes(v) { + result.Add(v) + } + } + return result +} + +// Difference returns a set with the elements that s has but +// other doesn't. +func (s Set) Difference(other Set) Set { + result := make(Set) + for k, v := range s { + var ok bool + if other != nil { + _, ok = other[k] + } + if !ok { + result.Add(v) + } + } + + return result +} + +// Some tests whether at least one element in the array passes the test implemented by the provided function. +// It returns a Boolean value. +func (s Set) Some(cb func(interface{}) bool) bool { + for _, v := range s { + if cb(v) { + return true + } + } + return false +} + +// Filter returns a set that contains the elements from the receiver +// where the given callback returns true. +func (s Set) Filter(cb func(interface{}) bool) Set { + result := make(Set) + + for _, v := range s { + if cb(v) { + result.Add(v) + } + } + + return result +} + +// Len is the number of items in the set. +func (s Set) Len() int { + return len(s) +} + +// List returns the list of set elements. +func (s Set) List() []interface{} { + if s == nil { + return nil + } + + r := make([]interface{}, 0, len(s)) + for _, v := range s { + r = append(r, v) + } + + return r +} + +// UnsafeListOfStrings dangerously casts list to a string +func (s Set) UnsafeListOfStrings() []string { + if s == nil { + return nil + } + + r := make([]string, 0, len(s)) + for _, v := range s { + r = append(r, v.(string)) + } + + return r +} + +// Copy returns a shallow copy of the set. +func (s Set) Copy() Set { + c := make(Set) + for k, v := range s { + c[k] = v + } + return c +} diff --git a/cli/internal/util/set_test.go b/cli/internal/util/set_test.go new file mode 100644 index 0000000..52736b4 --- /dev/null +++ b/cli/internal/util/set_test.go @@ -0,0 +1,149 @@ +package util + +import ( + "fmt" + "testing" +) + +func TestSetDifference(t *testing.T) { + cases := []struct { + Name string + A, B []interface{} + Expected []interface{} + }{ + { + "same", + []interface{}{1, 2, 3}, + []interface{}{3, 1, 2}, + []interface{}{}, + }, + + { + "A has extra elements", + []interface{}{1, 2, 3}, + []interface{}{3, 2}, + []interface{}{1}, + }, + + { + "B has extra elements", + []interface{}{1, 2, 3}, + []interface{}{3, 2, 1, 4}, + []interface{}{}, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { + one := make(Set) + two := make(Set) + expected := make(Set) + for _, v := range tc.A { + one.Add(v) + } + for _, v := range tc.B { + two.Add(v) + } + for _, v := range tc.Expected { + expected.Add(v) + } + + actual := one.Difference(two) + match := actual.Intersection(expected) + if match.Len() != expected.Len() { + t.Fatalf("bad: %#v", actual.List()) + } + }) + } +} + +func TestSetFilter(t *testing.T) { + cases := []struct { + Input []interface{} + Expected []interface{} + }{ + { + []interface{}{1, 2, 3}, + []interface{}{1, 2, 3}, + }, + + { + []interface{}{4, 5, 6}, + []interface{}{4}, + }, + + { + []interface{}{7, 8, 9}, + []interface{}{}, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%#v", i, tc.Input), func(t *testing.T) { + input := make(Set) + expected := make(Set) + for _, v := range tc.Input { + input.Add(v) + } + for _, v := range tc.Expected { + expected.Add(v) + } + + actual := input.Filter(func(v interface{}) bool { + return v.(int) < 5 + }) + match := actual.Intersection(expected) + if match.Len() != expected.Len() { + t.Fatalf("bad: %#v", actual.List()) + } + }) + } +} + +func TestSetCopy(t *testing.T) { + a := make(Set) + a.Add(1) + a.Add(2) + + b := a.Copy() + b.Add(3) + + diff := b.Difference(a) + + if diff.Len() != 1 { + t.Fatalf("expected single diff value, got %#v", diff) + } + + if !diff.Includes(3) { + t.Fatalf("diff does not contain 3, got %#v", diff) + } + +} + +func makeSet(n int) Set { + ret := make(Set, n) + for i := 0; i < n; i++ { + ret.Add(i) + } + return ret +} + +func BenchmarkSetIntersection_100_100000(b *testing.B) { + small := makeSet(100) + large := makeSet(100000) + + b.ResetTimer() + for n := 0; n < b.N; n++ { + small.Intersection(large) + } +} + +func BenchmarkSetIntersection_100000_100(b *testing.B) { + small := makeSet(100) + large := makeSet(100000) + + b.ResetTimer() + for n := 0; n < b.N; n++ { + large.Intersection(small) + } +} diff --git a/cli/internal/util/status.go b/cli/internal/util/status.go new file mode 100644 index 0000000..23ae165 --- /dev/null +++ b/cli/internal/util/status.go @@ -0,0 +1,47 @@ +package util + +import "fmt" + +// CachingStatus represents the api server's perspective +// on whether remote caching should be allowed +type CachingStatus int + +const ( + // CachingStatusDisabled indicates that the server will not accept or serve artifacts + CachingStatusDisabled CachingStatus = iota + // CachingStatusEnabled indicates that the server will accept and serve artifacts + CachingStatusEnabled + // CachingStatusOverLimit indicates that a usage limit has been hit and the + // server will temporarily not accept or serve artifacts + CachingStatusOverLimit + // CachingStatusPaused indicates that a customer's spending has been paused and the + // server will temporarily not accept or serve artifacts + CachingStatusPaused +) + +// CachingStatusFromString parses a raw string to a caching status enum value +func CachingStatusFromString(raw string) (CachingStatus, error) { + switch raw { + case "disabled": + return CachingStatusDisabled, nil + case "enabled": + return CachingStatusEnabled, nil + case "over_limit": + return CachingStatusOverLimit, nil + case "paused": + return CachingStatusPaused, nil + default: + return CachingStatusDisabled, fmt.Errorf("unknown caching status: %v", raw) + } +} + +// CacheDisabledError is an error used to indicate that remote caching +// is not available. +type CacheDisabledError struct { + Status CachingStatus + Message string +} + +func (cd *CacheDisabledError) Error() string { + return cd.Message +} diff --git a/cli/internal/util/task_id.go b/cli/internal/util/task_id.go new file mode 100644 index 0000000..e4415b6 --- /dev/null +++ b/cli/internal/util/task_id.go @@ -0,0 +1,66 @@ +package util + +import ( + "fmt" + "strings" +) + +const ( + // TaskDelimiter separates a package name from a task name in a task id + TaskDelimiter = "#" + // RootPkgName is the reserved name that specifies the root package + RootPkgName = "//" +) + +// GetTaskId returns a package-task identifier (e.g @feed/thing#build). +func GetTaskId(pkgName interface{}, target string) string { + if IsPackageTask(target) { + return target + } + return fmt.Sprintf("%v%v%v", pkgName, TaskDelimiter, target) +} + +// RootTaskID returns the task id for running the given task in the root package +func RootTaskID(target string) string { + return GetTaskId(RootPkgName, target) +} + +// GetPackageTaskFromId returns a tuple of the package name and target task +func GetPackageTaskFromId(taskId string) (packageName string, task string) { + arr := strings.Split(taskId, TaskDelimiter) + return arr[0], arr[1] +} + +// RootTaskTaskName returns the task portion of a root task taskID +func RootTaskTaskName(taskID string) string { + return strings.TrimPrefix(taskID, RootPkgName+TaskDelimiter) +} + +// IsPackageTask returns true if input is a package-specific task +// whose name has a length greater than 0. +// +// Accepted: myapp#build +// Rejected: #build, build +func IsPackageTask(task string) bool { + return strings.Index(task, TaskDelimiter) > 0 +} + +// IsTaskInPackage returns true if the task does not belong to a different package +// note that this means unscoped tasks will always return true +func IsTaskInPackage(task string, packageName string) bool { + if !IsPackageTask(task) { + return true + } + packageNameExpected, _ := GetPackageTaskFromId(task) + return packageNameExpected == packageName +} + +// StripPackageName removes the package portion of a taskID if it +// is a package task. Non-package tasks are returned unmodified +func StripPackageName(taskID string) string { + if IsPackageTask(taskID) { + _, task := GetPackageTaskFromId(taskID) + return task + } + return taskID +} diff --git a/cli/internal/util/task_output_mode.go b/cli/internal/util/task_output_mode.go new file mode 100644 index 0000000..eee42e0 --- /dev/null +++ b/cli/internal/util/task_output_mode.go @@ -0,0 +1,100 @@ +package util + +import ( + "encoding/json" + "fmt" +) + +// TaskOutputMode defines the ways turbo can display task output during a run +type TaskOutputMode int + +const ( + // FullTaskOutput will show all task output + FullTaskOutput TaskOutputMode = iota + // NoTaskOutput will hide all task output + NoTaskOutput + // HashTaskOutput will display turbo-computed task hashes + HashTaskOutput + // NewTaskOutput will show all new task output and turbo-computed task hashes for cached output + NewTaskOutput + // ErrorTaskOutput will show task output for failures only; no cache miss/hit messages are emitted + ErrorTaskOutput +) + +const ( + fullTaskOutputString = "full" + noTaskOutputString = "none" + hashTaskOutputString = "hash-only" + newTaskOutputString = "new-only" + errorTaskOutputString = "errors-only" +) + +// TaskOutputModeStrings is an array containing the string representations for task output modes +var TaskOutputModeStrings = []string{ + fullTaskOutputString, + noTaskOutputString, + hashTaskOutputString, + newTaskOutputString, + errorTaskOutputString, +} + +// FromTaskOutputModeString converts a task output mode's string representation into the enum value +func FromTaskOutputModeString(value string) (TaskOutputMode, error) { + switch value { + case fullTaskOutputString: + return FullTaskOutput, nil + case noTaskOutputString: + return NoTaskOutput, nil + case hashTaskOutputString: + return HashTaskOutput, nil + case newTaskOutputString: + return NewTaskOutput, nil + case errorTaskOutputString: + return ErrorTaskOutput, nil + } + + return FullTaskOutput, fmt.Errorf("invalid task output mode: %v", value) +} + +// ToTaskOutputModeString converts a task output mode enum value into the string representation +func ToTaskOutputModeString(value TaskOutputMode) (string, error) { + switch value { + case FullTaskOutput: + return fullTaskOutputString, nil + case NoTaskOutput: + return noTaskOutputString, nil + case HashTaskOutput: + return hashTaskOutputString, nil + case NewTaskOutput: + return newTaskOutputString, nil + case ErrorTaskOutput: + return errorTaskOutputString, nil + } + + return "", fmt.Errorf("invalid task output mode: %v", value) +} + +// UnmarshalJSON converts a task output mode string representation into an enum +func (c *TaskOutputMode) UnmarshalJSON(data []byte) error { + var rawTaskOutputMode string + if err := json.Unmarshal(data, &rawTaskOutputMode); err != nil { + return err + } + + taskOutputMode, err := FromTaskOutputModeString(rawTaskOutputMode) + if err != nil { + return err + } + + *c = taskOutputMode + return nil +} + +// MarshalJSON converts a task output mode to its string representation +func (c TaskOutputMode) MarshalJSON() ([]byte, error) { + outputModeString, err := ToTaskOutputModeString(c) + if err != nil { + return nil, err + } + return json.Marshal(outputModeString) +} -- cgit v1.2.3-70-g09d2