Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cmd/mxcli/docker/localapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type LocalApp struct {
RuntimeLogPath string

serve *ServeServer
// rec runs Rebuild's builds with the recovery `run --local --watch` has: a
// build whose outcome only a fresh mxbuild --serve fixes is repeated on one.
rec *serveRecovery
}

func (o *LocalAppOptions) applyDefaults() {
Expand Down Expand Up @@ -323,6 +326,7 @@ func StartLocalApp(opts LocalAppOptions) (*LocalApp, error) {
return nil, fmt.Errorf("starting mxbuild serve: %w", err)
}
app.serve = serve
app.rec = &serveRecovery{serve: serve, deployDir: opts.DeployDir, out: w, headless: true}

build, err := buildRetryingServeTimeout(serve, BuildRequest{Target: TargetDeploy, ProjectFilePath: opts.ProjectPath}, w)
if err != nil {
Expand Down Expand Up @@ -365,7 +369,14 @@ func (a *LocalApp) Rebuild(projectPath string) (ApplyAction, *BuildResult, error
if a.Runtime == nil {
return ActionReload, nil, fmt.Errorf("the runtime is not running")
}
build, err := a.serve.Build(BuildRequest{Target: TargetDeploy, ProjectFilePath: projectPath})
req := BuildRequest{Target: TargetDeploy, ProjectFilePath: projectPath}
var build *BuildResult
var err error
if a.rec != nil {
build, err = a.rec.Build(req)
} else {
build, err = a.serve.Build(req)
}
if err != nil {
return ActionReload, nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/mxcli/docker/serve_recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ type serveRecovery struct {
// failed. A theme failure then is not trustworthy: the process may be
// reporting the earlier error rather than the source on disk.
themeFailed bool

// headless: nothing serves the client (the test runner), so a build that
// succeeds with an incomplete web/dist is a good build and is not repeated.
headless bool
}

func (r *serveRecovery) Build(req BuildRequest) (*BuildResult, error) {
Expand Down Expand Up @@ -105,7 +109,7 @@ func (r *serveRecovery) Build(req BuildRequest) (*BuildResult, error) {
}

if res.OK() {
if !prebuilt {
if !prebuilt || r.headless {
return res, nil
}
after, _ := readPrebuiltBundle(r.deployDir)
Expand Down
21 changes: 21 additions & 0 deletions cmd/mxcli/docker/serve_recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,27 @@ func TestServeRecovery_RollupShapeIsNotServesBusiness(t *testing.T) {
}
}

// headless (mxcli test --watch): nothing serves the client, so an incomplete
// prebuilt bundle after a successful build is not a defect to recover from —
// recovering it would restart mxbuild --serve (and rebuild, and reboot the
// runtime) on every single test-file edit for a symptom the test runner never
// observes.
func TestServeRecovery_HeadlessSkipsPrebuiltRestart(t *testing.T) {
deploy := prebuiltDeployment(t)
f := &scriptedServe{build: func(_, _ int) (*BuildResult, error) {
writeIncompleteBundle(t, deploy) // the same symptom IncompletePrebuiltBundle recovers from
return buildOK(false), nil
}}
r := &serveRecovery{serve: f, deployDir: deploy, out: io.Discard, headless: true}
res, err := r.Build(BuildRequest{})
if err != nil || !res.OK() {
t.Fatalf("Build = %+v, %v", res, err)
}
if f.restarts != 0 {
t.Fatalf("restarts = %d, want 0: headless must not pay for a restart only a browser-facing bundle needs", f.restarts)
}
}

// The same serve process FAILS a change that regenerates a page: it wants
// web/pages/<Page>.js, which its own first build did not leave behind. Measured
// on 11.12.5 as well as 11.14. A fresh process builds it.
Expand Down
11 changes: 10 additions & 1 deletion cmd/mxcli/docker/webclient_legacy_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ func legacyClientBuildHint(deployDir, message, raw string) string {
// missingClientDirs returns the per-document client directories the failure
// names and the deployment genuinely lacks.
func missingClientDirs(deployDir, message string) []string {
if !strings.Contains(message, "Could not find a part of the path") {
// macOS and Linux builds of mxbuild report the same failure with the resource
// key instead of the English text: "AggregateException_ctor_DefaultMessage
// (IO_PathNotFound_Path, <path>)" (measured on 11.15.0, macOS).
byKey := strings.Contains(message, "IO_PathNotFound_Path")
if !strings.Contains(message, "Could not find a part of the path") && !byKey {
return nil
}
var named []string
Expand All @@ -115,6 +119,11 @@ func missingClientDirs(deployDir, message string) []string {
}
if _, err := os.Stat(filepath.Join(deployDir, "web", dir)); os.IsNotExist(err) {
named = append(named, "web/"+dir+"/")
} else if byKey {
// The key form names a file, not a directory, and the directory can exist
// (an earlier mxbuild left some pages) while the file the build wants is
// not there: the same first-build-only state.
named = append(named, "web/"+dir+"/ files")
}
}
return named
Expand Down
43 changes: 43 additions & 0 deletions cmd/mxcli/docker/webclient_legacy_paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ const (
`"status":"Failure","message":"Compilation of the app bundle failed."}`

bundleFailedMessage = "Compilation of the app bundle failed."

// Second serve build after an ALTER PAGE edit, macOS mxbuild 11.15.0: the
// same failure, reported by resource key instead of English text.
real1115MacOSMissingClientDirsByKey = "One or more errors occurred. " +
"(AggregateException_ctor_DefaultMessage (IO_PathNotFound_Path, " +
"/w/W1115/deployment/web/pages/MyFirstModule.Home_Web.js)) " +
"(AggregateException_ctor_DefaultMessage (IO_PathNotFound_Path, " +
"/w/W1115/deployment/web/layouts/Atlas_Core.Atlas_TopBar.js))"
)

// deploy1114 is the shape a 11.14 first build leaves: web/dist and nothing
Expand Down Expand Up @@ -77,6 +85,41 @@ func TestLegacyClientBuildHintExplainsBothRealFailures(t *testing.T) {
}
}

// macOS (and Linux) mxbuild report the identical first-build-only failure by
// resource key, not English text (measured on 11.15.0). The English-text match
// alone would leave --watch reporting "could not be applied" forever on these
// platforms, since missingClientDirs would never recognize the failure.
func TestLegacyClientBuildHintRecognizesMacOSKeyForm(t *testing.T) {
hint := legacyClientBuildHint(deploy1114(t), real1115MacOSMissingClientDirsByKey, "")
if hint == "" {
t.Fatal("must be explained: this is the same shape as the English-text message")
}
for _, want := range []string{"not a problem with your model", "web/pages/", "run --local --screenshot"} {
if !strings.Contains(hint, want) {
t.Errorf("hint missing %q:\n%s", want, hint)
}
}
}

// The key form names a FILE ("…/web/pages/X.js"), not the directory. When an
// earlier serve build left the directory behind (e.g. mid-session after one
// successful incremental build), a plain directory-exists check would wrongly
// conclude nothing is missing and the hint would stay silent on a build that
// is failing for exactly this reason.
func TestLegacyClientBuildHintMacOSKeyForm_DirExistsFileMissing(t *testing.T) {
dir := deploy1114(t)
if err := os.MkdirAll(filepath.Join(dir, "web", "pages"), 0o755); err != nil {
t.Fatal(err)
}
hint := legacyClientBuildHint(dir, real1115MacOSMissingClientDirsByKey, "")
if hint == "" {
t.Fatal("must still be explained: the directory exists but the file mxbuild wants does not")
}
if !strings.Contains(hint, "web/pages/ files") {
t.Errorf("hint must name the missing files, not just the directory:\n%s", hint)
}
}

func TestLegacyClientBuildHintStaysSilentOnOtherFailures(t *testing.T) {
dir := deploy1114(t)
// Every one of these is a real failure with a different cause. A hint on
Expand Down
Loading