github.com/grafana/pyroscope@v1.18.0/examples/examples_test.go (about) 1 //go:build examples 2 3 package examples 4 5 import ( 6 "bufio" 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "os" 15 "path/filepath" 16 "strings" 17 "syscall" 18 "testing" 19 "time" 20 21 "os/exec" 22 23 "github.com/stretchr/testify/require" 24 "gopkg.in/yaml.v3" 25 ) 26 27 const ( 28 timeoutPerExample = 10 * time.Minute 29 durationToStayRunning = 5 * time.Second 30 ) 31 32 type env struct { 33 dir string // project dir of docker-compose 34 path string // path to docker-compose file 35 } 36 37 type status struct { 38 Name string `json:"Name"` 39 State string `json:"State"` 40 } 41 42 func (e *env) projectName() string { 43 h := sha256.New() 44 _, _ = h.Write([]byte(e.dir)) 45 return fmt.Sprintf("%s_%x", filepath.Base(e.dir), h.Sum(nil)[0:2]) 46 } 47 48 func (e *env) newCmd(ctx context.Context, args ...string) *exec.Cmd { 49 c := exec.CommandContext( 50 ctx, 51 "docker", 52 append([]string{ 53 "compose", 54 "--file", e.path, 55 "--project-directory", e.dir, 56 "--project-name", e.projectName(), 57 }, args...)...) 58 return c 59 } 60 61 func (e *env) newCmdWithOutputCapture(t testing.TB, ctx context.Context, args ...string) *exec.Cmd { 62 c := e.newCmd(ctx, args...) 63 stdout, err := c.StdoutPipe() 64 require.NoError(t, err) 65 go func() { 66 scanner := bufio.NewScanner(stdout) 67 for scanner.Scan() { 68 t.Log(scanner.Text()) 69 } 70 }() 71 72 stderr, err := c.StderrPipe() 73 require.NoError(t, err) 74 go func() { 75 scanner := bufio.NewScanner(stderr) 76 for scanner.Scan() { 77 t.Log("STDERR: " + scanner.Text()) 78 } 79 }() 80 81 return c 82 } 83 84 func (e *env) containerStatus(ctx context.Context) ([]status, error) { 85 data, err := e.newCmd(ctx, "ps", "--all", "--format", "json").Output() 86 if err != nil { 87 return nil, err 88 } 89 90 var stats []status 91 dec := json.NewDecoder(bytes.NewReader(data)) 92 for { 93 var s status 94 err := dec.Decode(&s) 95 if errors.Is(err, io.EOF) { 96 break 97 } 98 if err != nil { 99 return nil, err 100 } 101 stats = append(stats, s) 102 } 103 104 return stats, nil 105 } 106 107 func (e *env) containersAllRunning(ctx context.Context) error { 108 status, err := e.containerStatus(ctx) 109 if err != nil { 110 return err 111 } 112 113 var errs []error 114 for _, s := range status { 115 if s.State != "running" { 116 errs = append(errs, fmt.Errorf("container %s is not running", s.Name)) 117 } 118 } 119 120 return errors.Join(errs...) 121 } 122 123 // removeExposedPorts removes ports from services which expose fixed ports. This will break once there is an overlap of ports. This will instead use random ports allocated by docker-compose. 124 func (e *env) removeExposedPorts(t testing.TB) *env { 125 126 var obj map[interface{}]interface{} 127 128 body, err := os.ReadFile(e.path) 129 if err != nil { 130 require.NoError(t, err) 131 } 132 133 if err := yaml.Unmarshal(body, &obj); err != nil { 134 require.NoError(t, err) 135 } 136 137 changed := false 138 139 for key, value := range obj { 140 if key.(string) == "services" { 141 services, ok := value.(map[string]interface{}) 142 if !ok { 143 require.NoError(t, fmt.Errorf("services is not a map[string]interface{}")) 144 } 145 for serviceName, service := range services { 146 params, ok := service.(map[string]interface{}) 147 if !ok { 148 require.NoError(t, fmt.Errorf("service '%s' is not a map[string]interface{}", serviceName)) 149 } 150 151 // check for ports 152 ports, ok := params["ports"] 153 if !ok { 154 continue 155 } 156 157 portsSlice, ok := ports.([]interface{}) 158 if !ok { 159 continue 160 } 161 for i := range portsSlice { 162 port, ok := portsSlice[i].(string) 163 if !ok { 164 continue 165 } 166 167 portSplitted := strings.Split(port, ":") 168 if len(portSplitted) < 2 { 169 continue 170 } 171 172 portsSlice[i] = portSplitted[len(portSplitted)-1] 173 changed = true 174 } 175 } 176 } 177 178 } 179 if !changed { 180 return e 181 } 182 183 path := filepath.Join(t.TempDir(), "docker-compose.yml") 184 data, err := yaml.Marshal(obj) 185 if err != nil { 186 require.NoError(t, err) 187 } 188 189 require.NoError(t, os.WriteFile(path, data, 0644)) 190 191 return &env{ 192 dir: e.dir, 193 path: path, 194 } 195 } 196 197 // This test is meant to catch very fundamental errors in the examples. It could be extened to be more comprehensive. For now it will just run the examples and check that they don't crash, within 5 seconds. 198 func TestDockerComposeBuildRun(t *testing.T) { 199 if testing.Short() { 200 t.Skip("skipping test in short mode.") 201 } 202 203 ctx := context.Background() 204 205 // find docker compose files 206 out, err := exec.Command("git", "ls-files", "**/docker-compose.yml").Output() 207 require.NoError(t, err) 208 209 var envs []*env 210 for _, path := range strings.Split(strings.TrimSpace(string(out)), "\n") { 211 e := &env{dir: filepath.Dir(path), path: path} 212 envs = append(envs, e) 213 } 214 215 for i := range envs { 216 t.Run(envs[i].dir, func(t *testing.T) { 217 e := envs[i] 218 t.Parallel() 219 ctx, cancel := context.WithTimeout(ctx, timeoutPerExample) 220 defer cancel() 221 t.Run("build", func(t *testing.T) { 222 cmd := e.newCmdWithOutputCapture(t, ctx, "build") 223 require.NoError(t, cmd.Run()) 224 }) 225 // run pull first so lcontainers can start immediately 226 t.Run("pull", func(t *testing.T) { 227 cmd := e.newCmdWithOutputCapture(t, ctx, "pull") 228 require.NoError(t, cmd.Run()) 229 }) 230 // now run the docker-compose containers, run them for 5 seconds, it would abort if one of the containers exits 231 t.Run("run", func(t *testing.T) { 232 ctx, cancel := context.WithCancel(ctx) 233 defer cancel() 234 e = e.removeExposedPorts(t) 235 cmd := e.newCmdWithOutputCapture(t, ctx, "up", "--abort-on-container-exit") 236 require.NoError(t, cmd.Start()) 237 238 // cleanup what ever happens 239 defer func() { 240 err := e.newCmdWithOutputCapture(t, context.Background(), "down", "--volumes").Run() 241 if err != nil { 242 t.Logf("cleanup error=%v\n", err) 243 } 244 }() 245 246 // check if all containers are still running after 5 seconds 247 go func() { 248 <-time.After(durationToStayRunning) 249 err := e.containersAllRunning(ctx) 250 if err != nil { 251 t.Logf("do nothing, as not all containers are running: %v\n", err) 252 return 253 } 254 t.Log("all healthy, start graceful shutdown") 255 err = cmd.Process.Signal(syscall.SIGTERM) 256 if err != nil { 257 t.Log("error sending terminate signal", err) 258 } 259 }() 260 261 err := cmd.Wait() 262 var exitError *exec.ExitError 263 if !errors.As(err, &exitError) || exitError.ExitCode() != 130 { 264 require.NoError(t, err) 265 } 266 267 }) 268 }) 269 } 270 271 }