blob: b4c6c8726b5f1f32549f5bb1fb6f65d28a153c58 [file] [log] [blame]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00001// Package experiment provides support for experimental features.
2package experiment
3
4import (
5 "fmt"
6 "io"
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -07007 "log/slog"
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -07008 "os"
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00009 "strings"
10 "sync"
11)
12
13// Experiment represents an experimental feature.
14// Experiments are global.
15type Experiment struct {
16 Name string // The name of the experiment used in -x flag
17 Description string // A short description of what the experiment does
18 Enabled bool // Whether the experiment is enabled
19}
20
21var (
22 mu sync.Mutex
23 experiments = []Experiment{
24 {
25 Name: "list",
26 Description: "List all available experiments and exit",
27 },
28 {
29 Name: "all",
30 Description: "Enable all experiments",
31 },
Josh Bleecher Snyder7f18fb62025-07-30 18:12:29 -070032 {
33 Name: "clipboard",
34 Description: "Enable enhanced clipboard functionality in patch tool",
35 },
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000036 }
37 byName = map[string]*Experiment{}
38)
39
40func Enabled(name string) bool {
41 mu.Lock()
42 defer mu.Unlock()
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -070043 e, ok := byName[name]
44 if !ok {
45 slog.Error("unknown experiment", "name", name)
46 return false
47 }
48 return e.Enabled
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000049}
50
51func init() {
Josh Bleecher Snydere01ea0e2025-05-06 02:03:26 +000052 for i := range experiments {
53 e := &experiments[i]
54 byName[e.Name] = e
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000055 }
56}
57
58func (e Experiment) String() string {
59 return fmt.Sprintf("\t%-15s %s\n", e.Name, e.Description)
60}
61
62// Fprint writes a list of all available experiments to w.
63func Fprint(w io.Writer) {
64 mu.Lock()
65 defer mu.Unlock()
66
67 fmt.Fprintln(w, "Available experiments:")
68 for _, e := range experiments {
69 fmt.Fprintln(w, e)
70 }
71}
72
73// Flag is a custom flag type that allows for comma-separated
74// values and can be used multiple times.
75type Flag struct {
76 Value string
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070077 set bool // whether the flag has been set explicitly
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000078}
79
80// String returns the string representation of the flag value.
81func (f *Flag) String() string {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070082 return f.Get().(string)
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000083}
84
85// Set adds a value to the flag.
86func (f *Flag) Set(value string) error {
Josh Bleecher Snyder085f74c2025-05-06 01:58:25 +000087 if f.Value == "" {
88 f.Value = value
89 } else {
90 f.Value = f.Value + "," + value // quadratic, doesn't matter, tiny N
91 }
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070092 f.set = true
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000093 return nil
94}
95
96// Get returns the flag values.
97func (f *Flag) Get() any {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070098 if f.set {
99 return f.Value
100 }
101 return os.Getenv("SKETCH_EXPERIMENT")
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000102}
103
104// Process handles all flag values, enabling the appropriate experiments.
105func (f *Flag) Process() error {
106 mu.Lock()
107 defer mu.Unlock()
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700108 v := f.String()
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000109
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700110 for name := range strings.SplitSeq(v, ",") {
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000111 name = strings.TrimSpace(name)
112 if name == "" {
113 continue
114 }
115 e, ok := byName[name]
116 if !ok {
117 return fmt.Errorf("unknown experiment: %q", name)
118 }
119 e.Enabled = true
120 }
121 if byName["all"].Enabled {
Josh Bleecher Snyderd42577f2025-05-05 17:59:00 -0700122 for i := range experiments {
123 e := &experiments[i]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000124 if e.Name == "list" {
125 continue
126 }
127 e.Enabled = true
128 }
129 }
130 return nil
131}