diff --git a/.gitignore b/.gitignore index 7bfe97f718e..f01544f105f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bin/ coverage.out covdatafiles/ .DS_Store +pkg/e2e/*.tar diff --git a/go.mod b/go.mod index a0307c30bb7..a1915b48b58 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.4.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect diff --git a/go.sum b/go.sum index f4f31c259b9..61c5d1379fa 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,9 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= diff --git a/pkg/compose/commit.go b/pkg/compose/commit.go index 520f795f9dd..f24d85e1cc5 100644 --- a/pkg/compose/commit.go +++ b/pkg/compose/commit.go @@ -41,21 +41,18 @@ func (s *composeService) commit(ctx context.Context, projectName string, options } name := getCanonicalContainerName(ctr) - msg := fmt.Sprintf("Commit %s", name) s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Working, - StatusText: "Committing", + ID: name, + Status: progress.Working, + Text: progress.StatusCommitting, }) if s.dryRun { s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Done, - StatusText: "Committed", + ID: name, + Status: progress.Done, + Text: progress.StatusCommitted, }) return nil @@ -73,10 +70,9 @@ func (s *composeService) commit(ctx context.Context, projectName string, options } s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Done, - StatusText: fmt.Sprintf("Committed as %s", response.ID), + ID: name, + Text: fmt.Sprintf("Committed as %s", response.ID), + Status: progress.Done, }) return nil diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index fc740de48a8..952e468c4c6 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -504,7 +504,9 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error()) return nil } - s.events.On(containerEvents(waitingFor, progress.ErrorEvent)...) + s.events.On(containerEvents(waitingFor, func(s string) progress.Event { + return progress.ErrorEventf(s, "dependency %s failed to start", dep) + })...) return fmt.Errorf("dependency failed to start: %w", err) } if healthy { @@ -532,7 +534,9 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr } msg := fmt.Sprintf("service %s", messageSuffix) - s.events.On(containerReasonEvents(waitingFor, progress.ErrorMessageEvent, msg)...) + s.events.On(containerEvents(waitingFor, func(s string) progress.Event { + return progress.ErrorEventf(s, "service %s", messageSuffix) + })...) return errors.New(msg) } default: @@ -600,9 +604,9 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro if err != nil { if ctx.Err() == nil { s.events.On(progress.Event{ - ID: eventName, - Status: progress.Error, - StatusText: err.Error(), + ID: eventName, + Status: progress.Error, + Text: err.Error(), }) } return ctr, err @@ -619,9 +623,9 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P defer func() { if err != nil && ctx.Err() == nil { s.events.On(progress.Event{ - ID: eventName, - Status: progress.Error, - StatusText: err.Error(), + ID: eventName, + Status: progress.Error, + Text: err.Error(), }) } }() diff --git a/pkg/compose/cp.go b/pkg/compose/cp.go index 4ae741af473..dcd03f2b1d2 100644 --- a/pkg/compose/cp.go +++ b/pkg/compose/cp.go @@ -86,24 +86,24 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a name := getCanonicalContainerName(ctr) var msg string if direction == fromService { - msg = fmt.Sprintf("copy %s:%s to %s", name, srcPath, dstPath) + msg = fmt.Sprintf("%s:%s to %s", name, srcPath, dstPath) } else { - msg = fmt.Sprintf("copy %s to %s:%s", srcPath, name, dstPath) + msg = fmt.Sprintf("%s to %s:%s", srcPath, name, dstPath) } s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Working, - StatusText: "Copying", + ID: name, + Text: progress.StatusCopying, + Details: msg, + Status: progress.Working, }) if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil { return err } s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Done, - StatusText: "Copied", + ID: name, + Text: progress.StatusCopied, + Details: msg, + Status: progress.Done, }) return nil }) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index efc05e283f8..495a0271bb7 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -1398,7 +1398,7 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty resp, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts) if err != nil { - s.events.On(progress.ErrorEvent(networkEventName)) + s.events.On(progress.ErrorEvent(networkEventName, err.Error())) return "", fmt.Errorf("failed to create network %s: %w", n.Name, err) } s.events.On(progress.CreatedEvent(networkEventName)) @@ -1632,7 +1632,7 @@ func (s *composeService) createVolume(ctx context.Context, volume types.VolumeCo DriverOpts: volume.DriverOpts, }) if err != nil { - s.events.On(progress.ErrorEvent(eventName)) + s.events.On(progress.ErrorEvent(eventName, err.Error())) return err } s.events.On(progress.CreatedEvent(eventName)) diff --git a/pkg/compose/down.go b/pkg/compose/down.go index 159c42a24c1..1136faf5d65 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -235,7 +235,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s if errdefs.IsNotFound(err) { continue } - s.events.On(progress.ErrorEvent(eventName)) + s.events.On(progress.ErrorEvent(eventName, err.Error())) return fmt.Errorf("failed to remove network %s: %w", name, err) } s.events.On(progress.RemovedEvent(eventName)) @@ -317,7 +317,7 @@ func (s *composeService) stopContainer(ctx context.Context, service *types.Servi timeoutInSecond := utils.DurationSecondToInt(timeout) err := s.apiClient().ContainerStop(ctx, ctr.ID, containerType.StopOptions{Timeout: timeoutInSecond}) if err != nil { - s.events.On(progress.ErrorMessageEvent(eventName, "Error while Stopping")) + s.events.On(progress.ErrorEvent(eventName, "Error while Stopping")) return err } s.events.On(progress.StoppedEvent(eventName)) @@ -360,7 +360,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain RemoveVolumes: volumes, }) if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) { - s.events.On(progress.ErrorMessageEvent(eventName, "Error while Removing")) + s.events.On(progress.ErrorEvent(eventName, "Error while Removing")) return err } s.events.On(progress.RemovedEvent(eventName)) diff --git a/pkg/compose/export.go b/pkg/compose/export.go index 85755352b36..c2c02d18f8b 100644 --- a/pkg/compose/export.go +++ b/pkg/compose/export.go @@ -51,13 +51,10 @@ func (s *composeService) export(ctx context.Context, projectName string, options } name := getCanonicalContainerName(container) - msg := fmt.Sprintf("export %s to %s", name, options.Output) - s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Working, - StatusText: "Exporting", + ID: name, + Text: progress.StatusExporting, + Status: progress.Working, }) responseBody, err := s.apiClient().ContainerExport(ctx, container.ID) @@ -67,12 +64,7 @@ func (s *composeService) export(ctx context.Context, projectName string, options defer func() { if err := responseBody.Close(); err != nil { - s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Error, - StatusText: fmt.Sprintf("Failed to close response body: %v", err), - }) + s.events.On(progress.ErrorEventf(name, "Failed to close response body: %s", err.Error())) } }() @@ -93,10 +85,9 @@ func (s *composeService) export(ctx context.Context, projectName string, options } s.events.On(progress.Event{ - ID: name, - Text: msg, - Status: progress.Done, - StatusText: "Exported", + ID: name, + Text: progress.StatusExported, + Status: progress.Done, }) return nil diff --git a/pkg/compose/kill.go b/pkg/compose/kill.go index 1b54cb61fbb..b730c6166cf 100644 --- a/pkg/compose/kill.go +++ b/pkg/compose/kill.go @@ -66,7 +66,7 @@ func (s *composeService) kill(ctx context.Context, projectName string, options a s.events.On(progress.KillingEvent(eventName)) err := s.apiClient().ContainerKill(ctx, ctr.ID, options.Signal) if err != nil { - s.events.On(progress.ErrorMessageEvent(eventName, "Error while Killing")) + s.events.On(progress.ErrorEvent(eventName, "Error while Killing")) return err } s.events.On(progress.KilledEvent(eventName)) diff --git a/pkg/compose/model.go b/pkg/compose/model.go index 7ae67321ca4..6a9363a4828 100644 --- a/pkg/compose/model.go +++ b/pkg/compose/model.go @@ -132,31 +132,30 @@ func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quiet if !quietPull { events.On(progress.Event{ - ID: model.Name, - Status: progress.Working, - Text: "Pulling", - StatusText: msg, + ID: model.Name, + Status: progress.Working, + Text: progress.StatusPulling, }) } } err = cmd.Wait() if err != nil { - events.On(progress.ErrorMessageEvent(model.Name, err.Error())) + events.On(progress.ErrorEvent(model.Name, err.Error())) } events.On(progress.Event{ - ID: model.Name, - Status: progress.Working, - StatusText: "Pulled", + ID: model.Name, + Status: progress.Working, + Text: progress.StatusPulled, }) return err } func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events progress.EventProcessor) error { events.On(progress.Event{ - ID: config.Name, - Status: progress.Working, - StatusText: "Configuring", + ID: config.Name, + Status: progress.Working, + Text: "Configuring", }) // configure [--context-size=] MODEL [-- ] args := []string{"configure"} diff --git a/pkg/compose/plugins.go b/pkg/compose/plugins.go index f0f1d9b030e..97bb604e9ef 100644 --- a/pkg/compose/plugins.go +++ b/pkg/compose/plugins.go @@ -143,7 +143,7 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty err = cmd.Wait() if err != nil { - s.events.On(progress.ErrorMessageEvent(service.Name, err.Error())) + s.events.On(progress.ErrorEvent(service.Name, err.Error())) return nil, fmt.Errorf("failed to %s service provider: %s", action, err.Error()) } switch command { diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index dddfb3faadc..1906f0521da 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -68,9 +68,10 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts for name, service := range project.Services { if service.Image == "" { s.events.On(progress.Event{ - ID: name, - Status: progress.Done, - Text: "Skipped - No image to be pulled", + ID: name, + Status: progress.Done, + Text: "Skipped", + Details: "No image to be pulled", }) continue } @@ -86,9 +87,10 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts case types.PullPolicyMissing, types.PullPolicyIfNotPresent: if imageAlreadyPresent(service.Image, images) { s.events.On(progress.Event{ - ID: "Image " + service.Image, - Status: progress.Done, - Text: "Skipped - Image is already present locally", + ID: "Image " + service.Image, + Status: progress.Done, + Text: "Skipped", + Details: "Image is already present locally", }) continue } @@ -96,9 +98,10 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts if service.Build != nil && opts.IgnoreBuildable { s.events.On(progress.Event{ - ID: "Image " + service.Image, - Status: progress.Done, - Text: "Skipped - Image can be built", + ID: "Image " + service.Image, + Status: progress.Done, + Text: "Skipped", + Details: "Image can be built", }) continue } @@ -119,11 +122,8 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts } if !opts.IgnoreFailures && service.Build == nil { if s.dryRun { - s.events.On(progress.Event{ - ID: "Image " + service.Image, - Status: progress.Error, - Text: fmt.Sprintf(" - Pull error for image: %s", service.Image), - }) + s.events.On(progress.ErrorEventf("Image "+service.Image, + "error pulling image: %s", service.Image)) } // fail fast if image can't be pulled nor built return err @@ -197,9 +197,9 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser if ctx.Err() != nil { s.events.On(progress.Event{ - ID: resource, - Status: progress.Warning, - StatusText: "Interrupted", + ID: resource, + Status: progress.Warning, + Text: "Interrupted", }) return "", nil } @@ -208,21 +208,15 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser // then the status should be warning instead of error if err != nil && service.Build != nil { s.events.On(progress.Event{ - ID: resource, - Status: progress.Warning, - Text: "Warning", - StatusText: getUnwrappedErrorMessage(err), + ID: resource, + Status: progress.Warning, + Text: getUnwrappedErrorMessage(err), }) return "", err } if err != nil { - s.events.On(progress.Event{ - ID: resource, - Status: progress.Error, - Text: "Error", - StatusText: getUnwrappedErrorMessage(err), - }) + s.events.On(progress.ErrorEvent(resource, getUnwrappedErrorMessage(err))) return "", err } @@ -442,13 +436,12 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr } events.On(progress.Event{ - ID: jm.ID, - ParentID: parent, - Current: current, - Total: total, - Percent: percent, - Text: jm.Status, - Status: status, - StatusText: text, + ID: jm.ID, + ParentID: parent, + Current: current, + Total: total, + Percent: percent, + Status: status, + Text: text, }) } diff --git a/pkg/compose/push.go b/pkg/compose/push.go index 54d4ef643a8..75e681e34ce 100644 --- a/pkg/compose/push.go +++ b/pkg/compose/push.go @@ -161,14 +161,13 @@ func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events progr } events.On(progress.Event{ - ParentID: prefix, - ID: jm.ID, - Text: jm.Status, - Status: status, - Current: current, - Total: total, - Percent: percent, - StatusText: text, + ParentID: prefix, + ID: jm.ID, + Text: text, + Status: status, + Current: current, + Total: total, + Percent: percent, }) } diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index 2acabe51dd6..5af53f60602 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -536,7 +536,7 @@ func TestBuildDependsOn(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "--progress=plain", "up", "test2") out := res.Combined() - assert.Check(t, strings.Contains(out, "test1 Built")) + assert.Check(t, strings.Contains(out, "test1 Built")) } func TestBuildSubset(t *testing.T) { @@ -548,7 +548,7 @@ func TestBuildSubset(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "build", "main") out := res.Combined() - assert.Check(t, strings.Contains(out, "main Built")) + assert.Check(t, strings.Contains(out, "main Built")) } func TestBuildDependentImage(t *testing.T) { @@ -560,11 +560,11 @@ func TestBuildDependentImage(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "firstbuild") out := res.Combined() - assert.Check(t, strings.Contains(out, "firstbuild Built")) + assert.Check(t, strings.Contains(out, "firstbuild Built")) res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "secondbuild") out = res.Combined() - assert.Check(t, strings.Contains(out, "secondbuild Built")) + assert.Check(t, strings.Contains(out, "secondbuild Built")) } func TestBuildSubDependencies(t *testing.T) { @@ -576,11 +576,11 @@ func TestBuildSubDependencies(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "build", "main") out := res.Combined() - assert.Check(t, strings.Contains(out, "main Built")) + assert.Check(t, strings.Contains(out, "main Built")) res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "up", "--build", "main") out = res.Combined() - assert.Check(t, strings.Contains(out, "main Built")) + assert.Check(t, strings.Contains(out, "main Built")) } func TestBuildLongOutputLine(t *testing.T) { @@ -592,11 +592,11 @@ func TestBuildLongOutputLine(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "build", "long-line") out := res.Combined() - assert.Check(t, strings.Contains(out, "long-line Built")) + assert.Check(t, strings.Contains(out, "long-line Built")) res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "up", "--build", "long-line") out = res.Combined() - assert.Check(t, strings.Contains(out, "long-line Built")) + assert.Check(t, strings.Contains(out, "long-line Built")) } func TestBuildDependentImageWithProfile(t *testing.T) { @@ -608,7 +608,7 @@ func TestBuildDependentImageWithProfile(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "build", "secret-build-test") out := res.Combined() - assert.Check(t, strings.Contains(out, "secret-build-test Built")) + assert.Check(t, strings.Contains(out, "secret-build-test Built")) } func TestBuildTLS(t *testing.T) { diff --git a/pkg/e2e/compose_run_test.go b/pkg/e2e/compose_run_test.go index 1bd53c51333..7ee2313aa2b 100644 --- a/pkg/e2e/compose_run_test.go +++ b/pkg/e2e/compose_run_test.go @@ -187,8 +187,8 @@ func TestLocalComposeRun(t *testing.T) { res.Assert(t, icmd.Success) res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/pull.yaml", "run", "--pull", "always", "backend") - assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulling"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulled"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulling"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulled"), res.Combined()) }) t.Run("compose run --env-from-file", func(t *testing.T) { diff --git a/pkg/e2e/networks_test.go b/pkg/e2e/networks_test.go index 68c8f5f4556..c9b882b9111 100644 --- a/pkg/e2e/networks_test.go +++ b/pkg/e2e/networks_test.go @@ -213,10 +213,10 @@ func TestNetworkRecreate(t *testing.T) { err := res.Stderr() fmt.Println(err) res.Assert(t, icmd.Expected{Err: ` - Container network_recreate-web-1 Stopped - Network network_recreate_test Removed - Network network_recreate_test Creating - Network network_recreate_test Created - Container network_recreate-web-1 Starting - Container network_recreate-web-1 Started`}) + Container network_recreate-web-1 Stopped + Network network_recreate_test Removed + Network network_recreate_test Creating + Network network_recreate_test Created + Container network_recreate-web-1 Starting + Container network_recreate-web-1 Started`}) } diff --git a/pkg/e2e/ps_test.go b/pkg/e2e/ps_test.go index f6910129e41..ae227ead164 100644 --- a/pkg/e2e/ps_test.go +++ b/pkg/e2e/ps_test.go @@ -38,7 +38,7 @@ func TestPs(t *testing.T) { _ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down") }) - assert.Contains(t, res.Combined(), "Container e2e-ps-busybox-1 Started", res.Combined()) + assert.Contains(t, res.Combined(), "Container e2e-ps-busybox-1 Started", res.Combined()) t.Run("table", func(t *testing.T) { res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps") diff --git a/pkg/e2e/pull_test.go b/pkg/e2e/pull_test.go index de3541bbd8e..799bdbb2fc7 100644 --- a/pkg/e2e/pull_test.go +++ b/pkg/e2e/pull_test.go @@ -34,15 +34,15 @@ func TestComposePull(t *testing.T) { res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/simple", "pull") output := res.Combined() - assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled")) - assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled")) + assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled")) + assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled")) // verify default policy is 'always' for pull command res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/simple", "pull") output = res.Combined() - assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled")) - assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled")) + assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled")) + assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled")) }) t.Run("Verify skipped pull if image is already present locally", func(t *testing.T) { @@ -52,16 +52,16 @@ func TestComposePull(t *testing.T) { res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/image-present-locally", "pull") output := res.Combined() - assert.Assert(t, strings.Contains(output, "alpine:3.13.12 Skipped - Image is already present locally")) + assert.Assert(t, strings.Contains(output, "alpine:3.13.12 Skipped Image is already present locally")) // image with :latest tag gets pulled regardless if pull_policy: missing or if_not_present - assert.Assert(t, strings.Contains(output, "alpine:latest Pulled")) + assert.Assert(t, strings.Contains(output, "alpine:latest Pulled")) }) t.Run("Verify skipped no image to be pulled", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/no-image-name-given", "pull") output := res.Combined() - assert.Assert(t, strings.Contains(output, "Skipped - No image to be pulled")) + assert.Assert(t, strings.Contains(output, "Skipped No image to be pulled")) }) t.Run("Verify pull failure", func(t *testing.T) { diff --git a/pkg/e2e/restart_test.go b/pkg/e2e/restart_test.go index 55e730c64a6..8b81e228a18 100644 --- a/pkg/e2e/restart_test.go +++ b/pkg/e2e/restart_test.go @@ -42,7 +42,7 @@ func TestRestart(t *testing.T) { c.RunDockerComposeCmd(t, "--project-name", projectName, "down") res := c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose.yaml", "--project-name", projectName, "up", "-d") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-restart-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-restart-1 Started"), res.Combined()) c.WaitForCmdResult(t, c.NewDockerComposeCmd(t, "--project-name", projectName, "ps", "-a", "--format", "json"), @@ -80,9 +80,9 @@ func TestRestartWithDependencies(t *testing.T) { res := c.RunDockerComposeCmd(t, "restart", baseService) out := res.Combined() - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Restarting", baseService)), out) - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out) - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Restarting", baseService)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out) assert.Assert(t, !strings.Contains(out, depNoRestart), out) c = NewParallelCLI(t, WithEnv( @@ -91,11 +91,11 @@ func TestRestartWithDependencies(t *testing.T) { )) res = c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d") out = res.Combined() - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Stopped", depWithRestart)), out) - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Recreated", baseService)), out) - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out) - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out) - assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Running", depNoRestart)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Stopped", depWithRestart)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Recreated", baseService)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out) + assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Running", depNoRestart)), out) } func TestRestartWithProfiles(t *testing.T) { @@ -111,5 +111,5 @@ func TestRestartWithProfiles(t *testing.T) { res := c.RunDockerComposeCmd(t, "restart", "test") fmt.Println(res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-profiles-test-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-profiles-test-1 Started"), res.Combined()) } diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 75d9298b284..19f07b71960 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -39,7 +39,7 @@ func TestStartStop(t *testing.T) { t.Run("Up a project", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "up", "-d") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-no-dependencies-simple-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-no-dependencies-simple-1 Started"), res.Combined()) res = c.RunDockerComposeCmd(t, "ls", "--all") testify.Regexp(t, getProjectRegx("running"), res.Stdout()) @@ -87,14 +87,14 @@ func TestStartStopWithDependencies(t *testing.T) { t.Run("Up", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName, "up", "-d") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined()) }) t.Run("stop foo", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop", "foo") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Stopped"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Stopped"), res.Combined()) res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running") assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-bar-1"), res.Combined()) @@ -103,12 +103,12 @@ func TestStartStopWithDependencies(t *testing.T) { t.Run("start foo", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Stopped"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Stopped"), res.Combined()) res = c.RunDockerComposeCmd(t, "--project-name", projectName, "start", "foo") out := res.Combined() - assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-bar-1 Started"), out) - assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-foo-1 Started"), out) + assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-bar-1 Started"), out) + assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-foo-1 Started"), out) res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running") out = res.Combined() @@ -120,8 +120,8 @@ func TestStartStopWithDependencies(t *testing.T) { _ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down") res := c.RunDockerComposeCmd(t, "-f", "./fixtures/links/compose.yaml", "--project-name", projectName, "up", "--no-deps", "-d", "foo") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined()) + assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined()) }) t.Run("down", func(t *testing.T) { @@ -136,8 +136,8 @@ func TestStartStopWithOneOffs(t *testing.T) { t.Run("Up", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName, "up", "-d") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-foo-1 Started"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-bar-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-foo-1 Started"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-bar-1 Started"), res.Combined()) }) t.Run("run one-off", func(t *testing.T) { @@ -213,7 +213,7 @@ func TestStopAlreadyStopped(t *testing.T) { // container is already stopped res.Assert(t, icmd.Expected{ ExitCode: 0, - Err: "Container e2e-start-stop-svc-already-stopped-simple-1 Stopped", + Err: "Container e2e-start-stop-svc-already-stopped-simple-1 Stopped", }) } @@ -230,14 +230,14 @@ func TestStartStopMultipleServices(t *testing.T) { res := cli.RunDockerComposeCmd(t, "stop", "simple", "another") services := []string{"simple", "another"} for _, svc := range services { - stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) + stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) } res = cli.RunDockerComposeCmd(t, "start", "simple", "another") for _, svc := range services { - startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) + startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) assert.Assert(t, strings.Contains(res.Stderr(), startMsg), fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) } @@ -256,7 +256,7 @@ func TestStartSingleServiceAndDependency(t *testing.T) { res := cli.RunDockerComposeCmd(t, "start", "desired") desiredServices := []string{"desired", "dep_1", "dep_2"} for _, s := range desiredServices { - startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s) + startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s) assert.Assert(t, strings.Contains(res.Combined(), startMsg), fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) } @@ -277,8 +277,8 @@ func TestStartStopMultipleFiles(t *testing.T) { cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/other.yaml", "up", "-d") res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-simple-1 Stopped"), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-another-1 Stopped"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-a-different-one-1 Stopped"), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-and-another-one-1 Stopped"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-simple-1 Stopped"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-another-1 Stopped"), res.Combined()) + assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-a-different-one-1 Stopped"), res.Combined()) + assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-and-another-one-1 Stopped"), res.Combined()) } diff --git a/pkg/e2e/up_test.go b/pkg/e2e/up_test.go index 395a1633736..36a1fc1f3b1 100644 --- a/pkg/e2e/up_test.go +++ b/pkg/e2e/up_test.go @@ -175,8 +175,8 @@ func TestUpWithAllResources(t *testing.T) { }) res := c.RunDockerComposeCmd(t, "-f", "./fixtures/resources/compose.yaml", "--all-resources", "--project-name", projectName, "up") - assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Volume %s_my_vol Created`, projectName)), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Network %s_my_net Created`, projectName)), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Volume %s_my_vol Created`, projectName)), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Network %s_my_net Created`, projectName)), res.Combined()) } func TestUpProfile(t *testing.T) { @@ -187,9 +187,9 @@ func TestUpProfile(t *testing.T) { }) res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/docker-compose.yaml", "--project-name", projectName, "up", "foo") - assert.Assert(t, strings.Contains(res.Combined(), `Container db_c Created`), res.Combined()) - assert.Assert(t, strings.Contains(res.Combined(), `Container foo_c Created`), res.Combined()) - assert.Assert(t, !strings.Contains(res.Combined(), `Container bar_c Created`), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), `Container db_c Created`), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), `Container foo_c Created`), res.Combined()) + assert.Assert(t, !strings.Contains(res.Combined(), `Container bar_c Created`), res.Combined()) } func TestUpImageID(t *testing.T) { @@ -218,7 +218,7 @@ func TestUpStopWithLogsMixed(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/stop/compose.yaml", "--project-name", projectName, "up", "--abort-on-container-exit") // assert we still get service2 logs after service 1 Stopped event res.Assert(t, icmd.Expected{ - Err: "Container compose-e2e-stop-logs-service1-1 Stopped", + Err: "Container compose-e2e-stop-logs-service1-1 Stopped", }) // assert we get stop hook logs res.Assert(t, icmd.Expected{Out: "service2-1 -> | stop hook running...\nservice2-1 | 64 bytes"}) diff --git a/pkg/progress/event.go b/pkg/progress/event.go index 4a9f6c95a18..5782b9345cc 100644 --- a/pkg/progress/event.go +++ b/pkg/progress/event.go @@ -16,7 +16,10 @@ package progress -import "context" +import ( + "context" + "fmt" +) // EventStatus indicates the status of an action type EventStatus int @@ -54,28 +57,52 @@ const ( StatusBuilt = "Built" StatusPulling = "Pulling" StatusPulled = "Pulled" + StatusCommitting = "Committing" + StatusCommitted = "Committed" + StatusCopying = "Copying" + StatusCopied = "Copied" + StatusExporting = "Exporting" + StatusExported = "Exported" ) // Event represents a progress event. type Event struct { - ID string - ParentID string - Text string - Status EventStatus - StatusText string - Current int64 - Percent int - Total int64 + ID string + ParentID string + Text string + Details string + Status EventStatus + Current int64 + Percent int + Total int64 +} + +func (e *Event) StatusText() string { + switch e.Status { + case Working: + return "Working" + case Warning: + return "Warning" + case Done: + return "Done" + default: + return "Error" + } } -// ErrorMessageEvent creates a new Error Event with message -func ErrorMessageEvent(id string, msg string) Event { - return NewEvent(id, Error, msg) +// ErrorEvent creates a new Error Event with message +func ErrorEvent(id string, msg string) Event { + return Event{ + ID: id, + Status: Error, + Text: StatusError, + Details: msg, + } } -// ErrorEvent creates a new Error Event -func ErrorEvent(id string) Event { - return NewEvent(id, Error, StatusError) +// ErrorEventf creates a new Error Event with format message +func ErrorEventf(id string, msg string, args ...any) Event { + return ErrorEvent(id, fmt.Sprintf(msg, args...)) } // CreatingEvent creates a new Create in progress Event @@ -181,18 +208,18 @@ func PulledEvent(id string) Event { // SkippedEvent creates a new Skipped Event func SkippedEvent(id string, reason string) Event { return Event{ - ID: id, - Status: Warning, - StatusText: "Skipped: " + reason, + ID: id, + Status: Warning, + Text: "Skipped: " + reason, } } // NewEvent new event -func NewEvent(id string, status EventStatus, statusText string) Event { +func NewEvent(id string, status EventStatus, text string) Event { return Event{ - ID: id, - Status: status, - StatusText: statusText, + ID: id, + Status: status, + Text: text, } } diff --git a/pkg/progress/json.go b/pkg/progress/json.go index 81601eab06a..8d48a890489 100644 --- a/pkg/progress/json.go +++ b/pkg/progress/json.go @@ -39,8 +39,9 @@ type jsonMessage struct { Tail bool `json:"tail,omitempty"` ID string `json:"id,omitempty"` ParentID string `json:"parent_id,omitempty"` - Text string `json:"text,omitempty"` Status string `json:"status,omitempty"` + Text string `json:"text,omitempty"` + Details string `json:"details,omitempty"` Current int64 `json:"current,omitempty"` Total int64 `json:"total,omitempty"` Percent int `json:"percent,omitempty"` @@ -54,8 +55,9 @@ func (p *jsonWriter) Event(e Event) { DryRun: p.dryRun, Tail: false, ID: e.ID, + Status: e.StatusText(), Text: e.Text, - Status: e.StatusText, + Details: e.Details, ParentID: e.ParentID, Current: e.Current, Total: e.Total, diff --git a/pkg/progress/json_test.go b/pkg/progress/json_test.go index d351963e553..dd6d2c32757 100644 --- a/pkg/progress/json_test.go +++ b/pkg/progress/json_test.go @@ -32,13 +32,13 @@ func TestJsonWriter_Event(t *testing.T) { } event := Event{ - ID: "service1", - ParentID: "project", - Text: "Creating", - StatusText: "Working", - Current: 50, - Total: 100, - Percent: 50, + ID: "service1", + ParentID: "project", + Status: Working, + Text: StatusCreating, + Current: 50, + Total: 100, + Percent: 50, } w.Event(event) @@ -50,8 +50,8 @@ func TestJsonWriter_Event(t *testing.T) { DryRun: true, ID: event.ID, ParentID: event.ParentID, - Text: event.Text, - Status: event.StatusText, + Text: StatusCreating, + Status: "Working", Current: event.Current, Total: event.Total, Percent: event.Percent, diff --git a/pkg/progress/plain.go b/pkg/progress/plain.go index 48129a46196..20132ad3349 100644 --- a/pkg/progress/plain.go +++ b/pkg/progress/plain.go @@ -43,7 +43,7 @@ func (p *plainWriter) Event(e Event) { if p.dryRun { prefix = api.DRYRUN_PREFIX } - _, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.StatusText) + _, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details) } func (p *plainWriter) On(events ...Event) { diff --git a/pkg/progress/tty.go b/pkg/progress/tty.go index 5545be67bf9..7e3d1f66bfd 100644 --- a/pkg/progress/tty.go +++ b/pkg/progress/tty.go @@ -44,6 +44,7 @@ func NewTTYWriter(out io.Writer) EventProcessor { type ttyWriter struct { out io.Writer + ids []string // tasks ids ordered as first event appeared tasks map[string]task repeated bool numLines int @@ -53,20 +54,21 @@ type ttyWriter struct { skipChildEvents bool operation string ticker *time.Ticker + suspended bool } type task struct { - ID string - parentID string - startTime time.Time - endTime time.Time - text string - status EventStatus - statusText string - current int64 - percent int - total int64 - spinner *Spinner + ID string + parentID string + startTime time.Time + endTime time.Time + text string + details string + status EventStatus + current int64 + percent int + total int64 + spinner *Spinner } func (t *task) stop() { @@ -110,7 +112,7 @@ func (w *ttyWriter) On(events ...Event) { w.mtx.Lock() defer w.mtx.Unlock() for _, e := range events { - if w.operation != "start" && (e.StatusText == "Started" || e.StatusText == "Starting") { + if w.operation != "start" && (e.Text == StatusStarted || e.Text == StatusStarting) { // skip those events to avoid mix with container logs continue } @@ -120,10 +122,12 @@ func (w *ttyWriter) On(events ...Event) { func (w *ttyWriter) event(e Event) { // Suspend print while a build is in progress, to avoid collision with buildkit Display - if e.StatusText == StatusBuilding { + if e.Text == StatusBuilding { w.ticker.Stop() - } else { + w.suspended = true + } else if w.suspended { w.ticker.Reset(100 * time.Millisecond) + w.suspended = false } if last, ok := w.tasks[e.ID]; ok { @@ -137,7 +141,7 @@ func (w *ttyWriter) event(e Event) { } last.status = e.Status last.text = e.Text - last.statusText = e.StatusText + last.details = e.Details // progress can only go up if e.Total > last.total { last.total = e.Total @@ -155,21 +159,22 @@ func (w *ttyWriter) event(e Event) { w.tasks[e.ID] = last } else { t := task{ - ID: e.ID, - parentID: e.ParentID, - startTime: time.Now(), - text: e.Text, - status: e.Status, - statusText: e.StatusText, - current: e.Current, - percent: e.Percent, - total: e.Total, - spinner: NewSpinner(), + ID: e.ID, + parentID: e.ParentID, + startTime: time.Now(), + text: e.Text, + details: e.Details, + status: e.Status, + current: e.Current, + percent: e.Percent, + total: e.Total, + spinner: NewSpinner(), } if e.Status == Done || e.Status == Error { t.stop() } w.tasks[e.ID] = t + w.ids = append(w.ids, e.ID) } w.printEvent(e) } @@ -191,7 +196,7 @@ func (w *ttyWriter) printEvent(e Event) { case Error: color = ErrorColor } - _, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, e.Text, color(e.StatusText)) + _, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details) } func (w *ttyWriter) print() { @@ -222,7 +227,7 @@ func (w *ttyWriter) print() { var statusPadding int for _, t := range w.tasks { - l := len(fmt.Sprintf("%s %s", t.ID, t.text)) + l := len(t.ID) if statusPadding < l { statusPadding = l } @@ -235,7 +240,9 @@ func (w *ttyWriter) print() { w.skipChildEvents = true } numLines := 0 - for _, t := range w.tasks { + + for _, id := range w.ids { // iterate on ids to enforce a consistent order + t := w.tasks[id] if t.parentID != "" { continue } @@ -286,7 +293,8 @@ func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding in // only show the aggregated progress while the root operation is in-progress if parent := t; parent.status == Working { - for _, child := range w.tasks { + for _, id := range w.ids { + child := w.tasks[id] if child.parentID == parent.ID { if child.status == Working && child.total == 0 { // we don't have totals available for all the child events @@ -305,20 +313,17 @@ func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding in hideDetails = true } - var txt string + txt := t.ID if len(completion) > 0 { - var details string + var progress string if !hideDetails { - details = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total))) + progress = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total))) } - txt = fmt.Sprintf("%s [%s]%s %s", + txt = fmt.Sprintf("%s [%s]%s", t.ID, SuccessColor(strings.Join(completion, "")), - details, - t.text, + progress, ) - } else { - txt = fmt.Sprintf("%s %s", t.ID, t.text) } textLen := len(txt) padding := statusPadding - textLen @@ -327,19 +332,20 @@ func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding in } // calculate the max length for the status text, on errors it // is 2-3 lines long and breaks the line formatting - maxStatusLen := terminalWidth - textLen - statusPadding - 15 - status := t.statusText + maxDetailsLen := terminalWidth - textLen - statusPadding - 15 + details := t.details // in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index - if maxStatusLen > 0 && len(status) > maxStatusLen { - status = status[:maxStatusLen] + "..." + if maxDetailsLen > 0 && len(details) > maxDetailsLen { + details = details[:maxDetailsLen] + "..." } - text := fmt.Sprintf("%s %s%s %s%s %s", + text := fmt.Sprintf("%s %s%s %s %s%s %s", pad, spinner(t), prefix, txt, strings.Repeat(" ", padding), - colorFn(t.status)(status), + colorFn(t.status)(t.text), + details, ) timer := fmt.Sprintf("%.1fs ", elapsed) o := align(text, TimerColor(timer), terminalWidth)