Skip to content

Fix Windows support: URI construction, file locks, native image CI, and test execution#504

Merged
oyvindberg merged 1 commit into
masterfrom
fix/zinc-retry-on-stale-analysis
Feb 22, 2026
Merged

Fix Windows support: URI construction, file locks, native image CI, and test execution#504
oyvindberg merged 1 commit into
masterfrom
fix/zinc-retry-on-stale-analysis

Conversation

@oyvindberg
Copy link
Copy Markdown
Owner

@oyvindberg oyvindberg commented Feb 19, 2026

Summary

Windows support (primary)

  • Fix URI construction everywhere: Replace all "file://" + path string concatenation with Path.toUri(). The old pattern produced illegal URIs on Windows (backslashes). Fixed in BuildLoader (4 sites), BspServer, MultiWorkspaceBspServer, and CoursierResolver.
  • Fix VirtualFile id on Windows: Normalize backslashes to forward slashes in PlainVirtualFile.id() so Zinc's javac pipeline can match class names to filenames.
  • Fix mandatory file lock conflict: Move .bleep-lock from target/classes/ to target/ — Windows mandatory file locks blocked Zinc from scanning class files in the same directory.
  • Retry on Windows sharing violations: ProjectLock retries with backoff when another process holds the lock file.
  • Propagate BSP errors: Surface BSP server errors to the client instead of silently succeeding with 0 compilations.
  • Drop -source 8 -target 8: bleep-test-runner no longer targets ancient Java, uses -proc:none like other projects.
  • Restore Windows CI: GraalVM setup, native image build, and test execution now run on Windows. Tests actually execute and pass (previously were silently a no-op on master).
  • Fix jar entry backslashes (temporary hack): PowerShell step to re-create jars with forward-slash entries after local publish. Should be fixed properly in bleep's jar creation code.

Compile progress & TUI

  • Compile sub-phases in TUI: Show zinc compilation stages (reading analysis, analyzing, compiling, saving analysis) with API count from loaded analysis.
  • ECJ compilation progress: ASM-generated bridge class for ECJ's CompilationProgress API, giving per-file progress and cooperative cancellation for Java compilation.
  • Fix progress jumping: Track max percent for monotonic progress (zinc resets current/total between compiler phases).
  • Fix empty error messages: Include failure error text when BSP diagnostics are empty; fix TaskDag error handler to preserve meaningful messages.
  • Fix TUI hang after cancel: Don't re-raise event consumer errors — build results are still valid even if progress notifications failed.
  • Fix ECJ cache corruption on cancel: Don't Thread.interrupt() ECJ threads — Java NIO closes channels on interrupt, poisoning JDK-level ZipFileSystem cache. Use CompilationProgress.isCancelled() instead.
  • Client context in BSP logs: Tag all server logs with [client => N] via logger.withContext so multi-client log interleaving is distinguishable.
  • Parallelism counter: Include currently running tasks in TUI parallelism display, not just finished ones.

Interrupted compilation detection

  • Sentinel file: Detect compilations interrupted by crash/kill and invalidate stale analysis.
  • HeapPressureGate: Back-pressure mechanism to pause new compilations when heap is under pressure.

Test plan

  • All CI jobs green (build, native images on Linux/macOS/Windows, IntelliJ plugin, yaml-ls)
  • Windows tests actually run and pass (first time ever in CI)
  • ECJ bridge tests pass (5 tests in EcjCompilationProgressBridgeTest)
  • Manual: compile large Java project, verify sub-phases visible in TUI
  • Manual: cancel mid-compile, recompile — no ClosedChannelException

🤖 Generated with Claude Code

@oyvindberg oyvindberg force-pushed the fix/zinc-retry-on-stale-analysis branch from 676557c to e508d6b Compare February 21, 2026 01:40
@oyvindberg oyvindberg changed the title Auto-retry Zinc compilation on stale incremental state BSP: compile sub-phases, ECJ progress, TUI fixes, and cancellation safety Feb 21, 2026
@oyvindberg oyvindberg force-pushed the fix/zinc-retry-on-stale-analysis branch from e449f3c to 391e535 Compare February 21, 2026 08:34
@oyvindberg oyvindberg changed the title BSP: compile sub-phases, ECJ progress, TUI fixes, and cancellation safety BSP improvements: compile progress, Windows fixes, TUI fixes, and cancellation safety Feb 22, 2026
@oyvindberg oyvindberg force-pushed the fix/zinc-retry-on-stale-analysis branch 4 times, most recently from 2194cf4 to 79625b7 Compare February 22, 2026 11:10
@oyvindberg oyvindberg changed the title BSP improvements: compile progress, Windows fixes, TUI fixes, and cancellation safety Fix Windows support: URI construction, file locks, native image CI, and test execution Feb 22, 2026
…nd test execution

Windows fixes:
- Replace all "file://" + path string concatenation with Path.toUri() — the old
  pattern produced illegal URIs on Windows (backslashes). Fixed in BuildLoader,
  BspServer, MultiWorkspaceBspServer, and CoursierResolver.
- Normalize backslashes in PlainVirtualFile.id() so Zinc's javac pipeline can
  match class names to filenames on Windows.
- Move .bleep-lock from target/classes/ to target/ — Windows mandatory file
  locks blocked Zinc from scanning class files in the same directory.
- Retry with backoff on Windows sharing violations in ProjectLock.
- Propagate BSP errors to client instead of silently succeeding with 0 compilations.
- Drop -source 8 -target 8 from bleep-test-runner.
- Restore Windows CI: GraalVM setup, native image build, and test execution.
  Tests actually run and pass (previously silently a no-op on master).

Compile progress & TUI:
- Show zinc compilation sub-phases in TUI (reading analysis, analyzing, compiling,
  saving analysis) with API count from loaded analysis.
- ECJ compilation progress via ASM-generated bridge class.
- Fix progress jumping (track max percent for monotonic progress).
- Fix empty error messages, fix TUI hang after cancel.
- Fix ECJ cache corruption on cancel (use cooperative cancellation instead of
  Thread.interrupt which poisons NIO ZipFileSystem cache).
- Tag BSP server logs with client context for multi-client disambiguation.

Interrupted compilation detection:
- Sentinel file to detect compilations interrupted by crash/kill.
- HeapPressureGate to pause new compilations when heap is under pressure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@oyvindberg oyvindberg force-pushed the fix/zinc-retry-on-stale-analysis branch from 79625b7 to 4696a3a Compare February 22, 2026 11:35
@oyvindberg oyvindberg merged commit 1886460 into master Feb 22, 2026
8 checks passed
@oyvindberg oyvindberg deleted the fix/zinc-retry-on-stale-analysis branch February 22, 2026 12:08
oyvindberg added a commit that referenced this pull request May 16, 2026
The `run_tests:` matrix field was introduced in 273e88e ("Skip tests on
macos-15-intel native image build (too slow for 20min timeout)", 2026-02-15)
and accumulated entries from #496, #504, #529 over time. Macos-15-intel and
macos-latest have been on `run_tests: false` since the Feb 2026 changes.

In #593's CI fix, Claude reused this existing mechanism to silently skip
ubuntu-22.04-arm without explicit permission — making it look like the arm64
native-image job was running tests when it wasn't. The justification
("Kotlin Native ships x86_64-only prebuilts") was real; the response was
wrong. Hiding the failure behind a matrix flag is not a fix.

This deletes the `run_tests` mechanism entirely: tests run on every arch in
the matrix. Where tests fail, we fix the test or the software. Loud comment
at the top of the matrix block keeps the policy machine-readable for the
next agent that tries this.
oyvindberg added a commit that referenced this pull request May 19, 2026
… restore arch coverage (#594)

* Run tests on every native-image arch (remove run_tests mechanism)

The `run_tests:` matrix field was introduced in 273e88e ("Skip tests on
macos-15-intel native image build (too slow for 20min timeout)", 2026-02-15)
and accumulated entries from #496, #504, #529 over time. Macos-15-intel and
macos-latest have been on `run_tests: false` since the Feb 2026 changes.

In #593's CI fix, Claude reused this existing mechanism to silently skip
ubuntu-22.04-arm without explicit permission — making it look like the arm64
native-image job was running tests when it wasn't. The justification
("Kotlin Native ships x86_64-only prebuilts") was real; the response was
wrong. Hiding the failure behind a matrix flag is not a fix.

This deletes the `run_tests` mechanism entirely: tests run on every arch in
the matrix. Where tests fail, we fix the test or the software. Loud comment
at the top of the matrix block keeps the policy machine-readable for the
next agent that tries this.

* Cancel Kotlin Native tests on linux-aarch64 via ScalaTest assume

JetBrains does not publish a `kotlin-native-prebuilt-linux-aarch64-*`
artifact (verified up to 2.3.21 and 2.4.0-RC). `KotlinNativeCompiler` falls
back to the `linux-x86_64` distribution on aarch64, which the JVM then
fails to load with `UnsatisfiedLinkError` inside
`kotlinx.cinterop.JvmCallbacksKt.<clinit>`.

Adds `PlatformTestHelper.assumeKotlinNativeAvailable()` keyed on
`OsArch.LinuxArm64`, and calls it at the top of the four tests that drive
the Konan compiler:

  - LinkExecutorIntegrationTest: "Kotlin Native test linking produces
    binary with test runner"
  - KotlinNativeAdvancedIntegrationTest: all three tests

ScalaTest reports these as canceled (not passed, not failed), so the
coverage gap is visible in the dashboard. When upstream ships the missing
prebuilt the helper becomes a no-op and the tests start running again.

* GenNativeImage: --emit-script for standalone native-image launcher

Two-phase native-image build for memory-constrained runners. With
`bleep native-image --emit-script <path>`, GenNativeImage builds the
manifest jar + assembles the full native-image command line, then writes
a self-contained launcher script and exits without running the build.
CI then shuts down the compile-server and executes the script so the
`native-image` tool inherits the full RAM budget (mattered most on the
mac arm runner, which previously hit the job timeout with bleep CLI +
BSP server + native-image fighting for ~7GB).

Script format auto-detected from extension: `.cmd`/`.bat` emits a Windows
batch file (CRLF endings, `cd /d`, `exit /b %ERRORLEVEL%`); anything else
emits a POSIX shell script (`set -euo pipefail`, `cd`, `exec`). Arguments
quoted defensively in both. POSIX path gets `chmod +x` (best-effort on
non-POSIX file stores).

Command-building logic replicates `NativeImagePlugin.nativeImage()` —
classpath fixed via the plugin's public `fixScala3` + bleep-core's
`fixedClasspath`; manifest jar written inline; remaining bits use the
plugin's existing public surface (`targetNativeImage{,Internal}`,
`nativeImageCommand`, `nativeImageOutput`). No submodule changes needed.

* CI: two-phase native-image — emit launcher, stop compile-server, run script

Non-Windows native-image steps now run:
  ./bleep-cli.sh --dev native-image --emit-script ni-build.sh <out>
  bleep config compile-server stop-all
  ./ni-build.sh

Windows native-image splits into three steps via the .cmd launcher.

The BSP server is now dead when `native-image` runs, releasing its heap
to the GraalVM tool which by default takes 80% of system RAM. Targets the
mac arm runner hitting the 40-min timeout under the prior single-phase
flow (bleep CLI + BSP server + `native-image` all live concurrently).

* KotlinNativeCompiler: route Konan prebuilt download through Coursier ArchiveCache

The Konan distribution (~200MB tarball) was being downloaded straight from
Maven Central via `URI.openConnection().getInputStream` into ~/.konan/,
then extracted by spawning `tar`. That path was invisible to Coursier so
the GitHub Actions `coursier/cache-action@v8` step couldn't cache it.
Every CI run re-downloaded 200MB per Kotlin version per host.

The metrics surfaced this: on the macos-15-intel run, the two top tests
(KotlinNativeIntegrationTest "resolves Kotlin/Native compiler embeddable
for 2.0.0" and "for 2.3.0") cost 95.6s and 83.9s respectively — almost
entirely download time. On mac-arm the same pattern pushes the
LinkExecutor / KotlinNativeAdvanced suites past the 2-minute test idle
timeout.

Now uses the same `BleepFileCache` + `ArchiveCache` path that
`FetchNode` / `FetchScalafmt` use. The tarball lands under
`~/.cache/coursier/arc/...`, which `coursier/cache-action@v8` already
includes in its cache key. Warm CI runs (and warm dev machines) skip the
download entirely.

Removes the `tar xzf` ProcessBuilder fork — Coursier's ArchiveCache
handles extraction (works on Windows / macOS / Linux without depending
on a host `tar`).

* Fix native-image script wiring: env-var trigger + URL filename + metrics guard

Three regressions from the previous push:

1. bleep's `Opts.arguments[String]()` rejected `--emit-script` as
   "Unexpected option". Switch to env-var trigger
   `BLEEP_NATIVE_IMAGE_EMIT_SCRIPT=<path>` so the workflow sets the path
   in `env:` and the script sees it. Removes the awkward CLI hack.

2. The Coursier ArchiveCache change reversed `<platform>` and
   `<version>` in the Maven Central URL: artifact is
   `kotlin-native-prebuilt-<VERSION>-<PLATFORM>.tar.gz` (classifier
   convention), but the extracted top-level folder is
   `kotlin-native-prebuilt-<PLATFORM>-<VERSION>`. Two separate names now.

3. `Collect BSP server metrics` ran with `if: always()` and shelled out
   to `./bleep` which doesn't exist when native-image failed. Falls back
   to the system bleep (`bleep` on PATH from bleep-setup-action) if the
   native binary isn't there. Windows step gets the same pattern via cmd
   `if exist`.

* Fix Windows cross-drive manifest entries + raise default test idle timeout to 5min

Two fixes from the latest CI:

1. Windows: GenNativeImage's manifest jar code did
   `manifestJar.getParent.relativize(path)` which throws
   IllegalArgumentException("'other' has different root") when classpath
   entries are on a different drive than the manifest jar — the default
   shape on GitHub Actions windows-latest where the workspace is on D:\
   but the Coursier cache is on C:\\Users\\…\\Coursier. Fall back to a
   `file:` URI for those entries (modern JDKs accept absolute URIs in
   Class-Path manifest attributes).

2. Default test idle timeout: 2 → 5 min. Mac native-image runs idle out
   on Kotlin/Native compile tests that legitimately take longer than 2
   minutes when Konan downloads + links without emitting interim events.
   KotlinNativeAdvancedIntegrationTest (3 tests, ~21s each on warm
   cache) and LinkExecutorIntegrationTest's Kotlin Native test all sat
   right at the 2-min ceiling. 5 min covers the worst case we see with
   margin; override via `~/.config/bleep/config.yaml`.

* Windows manifest absolute-path fallback + ignore the second cancel-race flake

Two findings from the latest CI:

1. Windows native-image rejected the `file:` URI I used as the cross-drive
   fallback in the manifest jar's Class-Path attribute:
     java.nio.file.InvalidPathException: Illegal char <:> at index 4:
     file:///C:/Users/RUNNER~1/AppData/Local/Temp/scala3Runtime...jar
   GraalVM's `handleClassPathAttribute` does `Path.of(token)` on each
   entry, which doesn't parse URIs. Switch the cross-drive fallback to a
   plain forward-slashed absolute path (`C:/Users/.../foo.jar`) — that's
   what `Path.of` accepts on Windows.

2. arm64 ubuntu flaked on the "immediate cancel" cancel test. Same race
   as the already-ignored huge-source cancel: cancel can lose to a
   Zinc-returns-Ok-with-0-classes outcome and we report Ok instead of
   Cancelled. Real bug, tracked alongside its sibling. Ignored for now so
   arm64 ubuntu (which already cancels the four KotlinNative tests for
   the lack of an aarch64 prebuilt) doesn't flake the run on a separate
   issue.

* Bump default test idle timeout 5 → 10 min for slow mac Konan runs

5 min was still tight on mac CI: LinkExecutorIntegrationTest's Kotlin/Native
test hit 313s on mac-arm and 349s on mac-intel in back-to-back runs, both
busy downloading the Konan prebuilt for the first time and emitting no
intermediate progress events. The macOS GitHub Actions runners vary enough
that 5 min sometimes fits and sometimes doesn't.

10 min keeps the safety net wide enough that genuine hangs still get
killed, but gives the slow legitimate path margin. Override via
~/.config/bleep/config.yaml if you need tighter.

* Revert DefaultTestIdleTimeoutMinutes 10 → 2

10-min default is a sign of papering over slow tests, not a healthy
posture. Reverting to 2 min — the correct ceiling for "a single test
should never sit silent that long". If a particular environment needs
more (cold mac CI hitting first-time Konan download was the empirical
trigger), override in `~/.config/bleep/config.yaml` per-environment.

With the Konan tarball now flowing through `~/.cache/coursier/arc` and
`coursier/cache-action@v8` snapshotting that dir between runs, subsequent
CI runs should hit a warm cache and avoid the slow path entirely.
Validating that on this push.

* CI: configure test idle timeout per-environment, not in code

`DefaultTestIdleTimeoutMinutes` stays at 2 min in code (the right
posture). CI's `~/.config/bleep/config.yaml` now sets it to 10 min for
both the `build` and `build-native-image` jobs — the Kotlin/Native LLVM
bitcode link on a cold-ish runner legitimately exceeds 2 min and that's
not a defect, it's just native compilation taking native time.

Build job: merges the new timeout into the existing parallelism config.
Native-image jobs: new step before the build step so any subsequent BSP
server invocation (incl. the test step that comes after native-image is
done) picks up the relaxed timeout.

Note: Windows uses bash for this step since the path expansion needs `$HOME`.

* CI: resolve bleep config path via `bleep config file --output raw`

`~/.config/bleep/config.yaml` is the Linux XDG path; on macOS bleep
reads `~/Library/Application Support/build.bleep/config.yaml` and on
Windows `%APPDATA%\build\bleep\config\config.yaml`. The previous
workflow step hardcoded the Linux path so the testIdleTimeoutMinutes
override never took effect on mac runners — they happily timed out at
2 min default.

Use `bleep config file --output raw` to print the actual path bleep
will read from, and write the config there. Works cross-platform via
bash (Git Bash is preinstalled on Windows runners).

* IntegrationTestHarness: sanitize test names in temp-dir paths

Test names with spaces / non-ASCII chars produced temp-dir paths like
`/tmp/bleep-doc-E. clean → recompile rebuilds generated sources
deterministically-…`. We've seen this same test get `rm -Rf` SIGKILLed
(exit 137) on three different CI runs on three different platforms.
JVM metrics show no heap pressure on the BSP server at the time, so
it's likely the test-runner JVM hitting its 512MB cap and the kernel
reaping its rm child as OOM cleanup. Keeping the path ASCII at least
removes one source of noise; if the flake persists we can chase the
real cause.

* IntegrationTestHarness: pin inner BSP parallelism to 1, split slow ITs

bleep-tests' outer `bleep test bleep-tests` runs effectiveParallelism = cores
ForkedTestRunner JVMs in parallel. Each IT internally spins up an in-process
BSP (`InProcessBspServer`) that creates its own `JvmPool.create(maxParallelism,
…)`. Without an explicit cap the inner pool also defaults to cores, so a single
IT could fork up to N more JVMs — cartesian explosion to N×N max.

The trace evidence: during KspToyProcessorIT's 2-minute idle-timeout window,
the OTLP trace showed 22+ concurrent ForkedTestRunner JVMs each at 300-900 MB
RSS. That's what was starving four heavy ITs (KspToyProcessorIT,
YourFirstScalaProjectIT, SourcegenIT, SourcegenKotlinIT) into the suite-idle
timeout — they're fine standalone (5-35 s) but couldn't make progress fast
enough under that JVM pressure to emit a test event before the timer fired.

Fix: pin testConfig.bspServerConfig.parallelism = Some(1) in
IntegrationTestHarness. Result: full bleep-tests run went from 234 passing + 4
timing out in ~351 s → 246 passing, 0 timing out in 161 s.

The IT splits (SourcegenIT, SourcegenKotlinIT, YourFirstScalaProjectIT) are
kept because they're semantically cleaner — smaller test methods give better
failure attribution and timer-reset granularity — even though the parallelism
fix made them no longer load-bearing for the timeout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* CI: bump native-image timeout 25 → 40 minutes

macos-latest (arm64) cancelled at 25:19 mid native-image build under the prior
25-minute ceiling, before the test phase even started. Other arches finish in
13-20 min, but mac-arm needs the headroom.

Split out of the (dropped) `CI: collect + upload server metrics` commit so we
keep just the timeout bump without the observability infrastructure on this
branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* SnapshotTest.gitWithRetry: retry git on transient .git/index.lock collision

Under heavy parallel-suite execution (10+ test runner JVMs forked from
`bleep test bleep-tests`), the snapshot suites (`RewriteSnapshotTest`,
`IntegrationSnapshotTests`, `CreateNewSnapshotTests`, `TemplateTest`) all do
`git add` against the outer bleep repo's `.git/index`. They serialize among
themselves via `GitLock` (a cross-process `FileChannel.lock()` on
`.git/bleep-test.lock`), but other writers we can't lock against — the test-
host JVM's `ProjectDigest.gitDirtyPaths` doing `git status --porcelain`
(refreshes the stat cache, takes index.lock briefly), or an editor / shell
the developer happens to have open — can still race.

Add an exponential-backoff retry around git invocations that catches
`BleepException.Text` whose message contains "index.lock". 10 attempts, base
100ms, so worst case ~5.5s before giving up. Any other git failure (real
diff mismatch, missing path, real error) propagates on the first attempt.

Both `git add` and `git diff` in `writeAndCompare` go through the same
helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* BuildDisplay: remove ryddig progressMonitor line-redraw

The "Compiling X: started, Y: 14%, Z: 67%" line that BuildDisplay used to push
through ryddig's `progressMonitor` was a single in-place updating line. In a
terminal it looks nice; in CI logs it leaks `\x1b[K` (ANSI erase-to-end-of-line)
escapes on every refresh, producing visible garbage when GitHub Actions
captures the line-by-line output. The TUI's full-screen mode is already
auto-disabled in CI; this was the leftover bit.

The per-event log lines we already emit cover the same lifecycle:
- `🔨 compiling (...)` via `CompilationReason` — what kicked the build off
- `📦 read analysis / analyzed / compiled / saved analysis (...ms)` — phases
- `✅ compiled (...ms)` or `❌ compile failed` via `CompileFinished`

Removed:
- `activeCompileProgress` mutable map
- `lastProgressLine` var
- `progressMonitor: Option[LoggerFn]` lookup
- `renderCompileProgress()` function
- Calls from `CompileStarted`/`CompileFinished`/`CompileProgress`
- Now-unused `ryddig.{LoggerFn, TypedLogger}` import

`CompileStarted` is now a no-op (the meaningful start is logged from
`CompilationReason`); `CompileProgress` is dropped on the floor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* TestRunner: inject NO_COLOR=1 into forked test JVMs

ScalaTest 3.2.16+ honors the no-color.org `NO_COLOR` env var; JUnit, JUnit 5,
sbt, gradle, mill, and most other JVM test frameworks do too. Inject it into
every forked test-runner JVM's environment so test output captured in CI logs
or bleep's server-metrics dashboard is plain text instead of ANSI-decorated.

A project-supplied `platform.jvmEnvironment.NO_COLOR` overrides this default
(Map.++ right-bias), so anyone who really wants colored test output can set it
empty in their bleep.yaml.

Done at `computeTestEnvironment` — one place, hits every test JVM. Other
forked subprocesses (KSP runner, native-image, …) are a follow-up if needed,
but test output is the noisiest channel in CI captures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Subprocess forks: default NO_COLOR=1, centralized at the spawn primitives

Three forking entry points cover every subprocess bleep spawns:
1. `BspServerOperations.startServer` — CLI → BSP daemon. Setting NO_COLOR on
   the daemon's own env propagates to all of its children via ProcessBuilder's
   default env-copy behavior, including the few sites that use raw
   `scala.sys.process.Process` (e.g. `ProjectDigest`'s `git status`).
2. `ProcessRunner.start` — all KSP / Kotlin & Scala JS-Native linkers /
   node / tar / native-image forks route through here.
3. `JvmPool` test-runner fork — direct `ProcessBuilder.start()`, doesn't go
   through ProcessRunner.

All three now `pb.environment().putIfAbsent("NO_COLOR", "1")`. `putIfAbsent`
preserves an explicit override (a project's `platform.jvmEnvironment.NO_COLOR`,
or the parent's inherited setting if a developer wants color in some specific
case).

ScalaTest 3.2.16+, JUnit, JUnit 5, sbt, gradle, mill, kotlinc/KSP, GraalVM
native-image, and most other JVM-side tools honor `NO_COLOR=1` per
no-color.org. End result: clean text in CI log captures and bleep's own
subprocess output panels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* PreBootstrapOpts: honor NO_COLOR env var (no-color.org)

Any non-empty `NO_COLOR` env var disables ANSI in bleep's own logger, matching
the no-color.org standard. Explicit `--no-color` on the CLI still wins (sets
the same flag deterministically).

Why this matters now: the BSP daemon's child sourcegen-script JVMs inherit
NO_COLOR=1 from the daemon's env (added in the previous commit), but their
PreBootstrapOpts.parse only looked at command-line args — they had no flag,
so they emitted colored/emoji output that the daemon forwarded through to the
CLI as ANSI-decorated text. Now those forked scripts auto-detect NO_COLOR=1
from env and use the plain log pattern.

Same chain: parent CLI's --no-color → NO_COLOR=1 in daemon env → inherited by
script forks → PreBootstrapOpts.parse picks up env → script logger uses plain
bracket prefixes. End to end, no ANSI leakage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* DisplayMode: --no-color / NO_COLOR also disables the TUI

A TUI is a colored fullscreen interface — running one when the user asked for
no colors is the wrong answer. Make `DisplayMode.fromFlags` consult both the
`--no-tui` flag and a new JVM-local "no-color was requested" marker that
`PreBootstrapOpts.parse` sets when it sees `--no-color` or a non-empty
`NO_COLOR` env var. Either route downgrades to `NoTui`.

`PreBootstrapOpts.noColorRequested` exposes the same answer to anyone in the
same JVM that needs it (the chief reader being `DisplayMode.fromFlags`; the
existing `LoggingOpts.noColor` already covers the logger). The marker is a
`bleep.noColor` system property set by `parse` so each invocation reflects
its own state without re-parsing args.

Reported: `./bleep-cli.sh test --no-color` still ran the TUI. After this,
the same command renders plain log lines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* BleepConsole: ANSI-toggle wrapper that respects no-color

Three files baked ANSI directly into log message strings (`s"\${C.RED}foo\${C.RESET}"`)
via `scala.Console`: `BuildDisplay`, `ReactiveBsp`, `CompileDisplay`. The
ryddig log pattern's `noColor` only strips ANSI it adds itself — it doesn't
strip what's already in the message body — so these survived `--no-color`.

New `bleep.testing.BleepConsole` mirrors the `scala.Console` field surface but
returns "" when no-color is in effect (per `PreBootstrapOpts.noColorRequested`).
The existing imports flip from `scala.{Console => SConsole/C}` to
`bleep.testing.BleepConsole as SConsole/C` — every call site continues to
write `SConsole.RED` / `C.GREEN`, just now ANSI-free in no-color mode.

The `on` flag is a class-loading-time val so it captures whatever
`PreBootstrapOpts.parse` decided. Pre-parse runs at the start of every bleep
JVM invocation before these objects are touched.

End-to-end with this + previous commits:
  --no-color  →  PreBootstrapOpts marks JVM no-color  →
                 - bleep's logger pattern: no ANSI prefix
                 - DisplayMode.fromFlags: NoTui
                 - BuildSummary / per-test / per-suite messages: no ANSI
                 - daemon/script/test-runner forks: NO_COLOR=1 in env

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* model.Project: add testTags field for cross-framework test tagging

A `JsonMap[String, JsonSet[String]]` field carrying tag-name → FQDN-pattern
mappings. Filtered at suite-dispatch in the BSP server (next commit), so the
mechanism is framework-independent: works for ScalaTest, JUnit, MUnit, utest,
anything the test runner discovers. Method-level tagging is out — tag at the
class level, with `*` / `**` glob patterns for convention tags like "all ITs".

Just the model + codec plumbing in this commit. SetLike methods
(intersect / removeAll / union / isEmpty) and `empty` all extended with the
new field. Codec is derived; new field surfaces automatically in the JSON
schema regeneration.

CLI surface (next commits): `bleep test --only-tag slow --exclude-tag flaky`,
mirroring the existing `--only` / `--exclude` regex flags. Open-ended tag
namespace; case-sensitive lowercase recommended.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* TestTagFilter: glob-pattern test-suite filter + ProjectGlobs.testTagsMap

Pure filter logic for the test-tagging feature, separated from BSP dispatch:

  - `compileGlob(pattern)` — single `*` stays within an FQDN segment (no
    dots), `**` spans dots. Regex metachars are escaped for plain segments.
    `bleep.foo.*Test` matches `bleep.foo.Bar` but not `bleep.foo.bar.Bar`;
    `**IT` matches any FQDN ending in IT regardless of package depth.

  - `tagsFor(suite, manifest)` — returns the set of tags that apply to a
    given suite FQDN given the project's testTags map.

  - `filter(suites, manifest, includeTags, excludeTags)` — applies the
    selection semantics: empty includes → all; non-empty includes → union
    of matching tags (untagged suites are dropped when an include is set);
    excludes always subtract.

  - `staleManifestEntries(manifest, discovered)` — surfaces patterns that
    match no discovered suite, for the validation warnings the user wanted
    in the build summary.

ProjectGlobs gets a `testTagsMap` member: union of every `testTags` key
across all build projects, in the shape decline's `Argument.fromMap` wants
for tab-completion + value validation.

12 unit tests covering all glob/filter/validation paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Test tags: wire --only-tag/--exclude-tag through CLI + BSP + list-tests

Adds the user-facing surface for the testTags manifest already declared on
model.Project. Flags work for any test framework because filtering happens at
bleep's suite-dispatch boundary in MultiWorkspaceBspServer, not via framework-
native tags.

- BleepBspProtocol.TestOptions gains includeTags/excludeTags fields
- ReactiveBsp threads them through; runOnce prunes candidate projects whose
  testTags declare none of the requested includes (saves compile work)
- MultiWorkspaceBspServer.discoverHandler applies TestTagFilter.filter on
  discovered suites against the project's testTags manifest
- Main.scala adds --only-tag/--exclude-tag with Argument.fromMap over
  ProjectGlobs.testTagsMap: strict validation + tab-completion
- ListTests annotates each discovered suite with matching tags and warns
  about stale manifest entries

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Tag *IT as slow; exclude from native-image test runs

- bleep.yaml: bleep-tests gets `testTags.slow: ["**IT"]`. Every IT-suffixed
  test in this project extends IntegrationTestHarness and spins up an
  in-process bleep-bsp running real builds end-to-end. 32 classes total.
- schema.json: hand-add testTags property under Project (yaml-ls-check
  reads schema.json, so failing to update it would warn on the new field).
- .github/workflows/build.yml: native-image jobs (ubuntu x86_64 + windows)
  now pass `--exclude-tag slow` to skip the IT bracket. Those jobs exist
  to validate that the produced binary runs, not to re-exercise the test
  surface. The `build` job is the canonical full-suite gate and continues
  to run everything.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Tag filter: integration tests + crystal-clear error messages

When a tag-related filter is misconfigured, the user used to get either no
feedback (silent project drop) or a one-liner that lied about "Available
suites" (it listed everything, ignoring what the tag filter had already
removed). Now the error walks through the pipeline so you can tell exactly
which stage emptied the set.

Sample error for `--only-tag slow` against a project whose only suite is
untagged:

    --only-tag matched no test suites in mytest (--only-tag slow):
    1 discovered → 0 after tag filter.
    Tags declared in mytest: slow
    Suites that survived --only/--exclude (none matched the tag filter):
    example.FastTest

The CLI also logs a one-line "pre-filtered N project(s)" notice when
`--only-tag` drops projects before BSP dispatch, so users notice why
their explicitly-listed project was skipped.

Tests:
- TestTagsIT (new, 5 cases): --only-tag runs only tagged; --exclude-tag
  drops tagged while keeping untagged; --only + --only-tag = AND
  semantics; empty-result error wording; project pre-filter doesn't
  throw, surfaces as info log.
- Commands.test API extended with includeTags/excludeTags (no defaults);
  all 12 existing callers updated to pass None.
- JCommands (Java surface) also updated.
- Error path: tag-side empty triggers the same TaskResult.Failure path
  --only used to own, with a richer message; --exclude-tag emptying the
  set is intentional and stays silent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Always show filter accounting in test summary

When a filter (--only, --exclude, --only-tag, --exclude-tag) is active OR when
--only-tag pre-filtered any project, the summary now ends with two new lines
under the existing Tests/Suites/Duration block:

    Projects: 1/2 selected (1 pre-filtered by --only-tag slow: bleep-bsp-tests)
    Filters active: --only NoSuchTest · --only-tag slow

"N/M selected" is in CrossProjectName terms — i.e. post-glob-expansion — because
that's the layer ReactiveBsp lives at. The user's typed globs (jvm3, prefixes)
are resolved by ProjectGlobs upstream, so they're not preserved at this layer;
the documentation on FilterContext spells this out.

Plumbing:
- New FilterContext case class in BuildDisplay.scala.
- BuildSummary gains `filterContext: Option[FilterContext]` (required field,
  defaults to None at every construction site per the no-defaults rule).
- BuildDisplay.printSummary signature changes to take `Option[FilterContext]`;
  both real impls plus the legacy ReactiveTestRunner path updated.
- ReactiveBsp builds the context in runOnce and threads it through
  runInProcess / runWithBleepBsp / printFinalSummary.
- BuildSummary.formatSummary renders the two new lines only when the filter
  did something the user might want to see.

Tests: TestTagsIT gains "summary reports projects-selected ratio..." plus log
assertions on the new lines for existing scenarios. Renamed one earlier IT to
drop a `/` that broke Files.createTempDirectory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: add test-tags usage page

Covers the testTags manifest syntax (glob semantics, single vs array values),
CLI surface (--only-tag / --exclude-tag with union/subtractive rules + strict
validation), project pre-filter optimization, list-tests inspection, summary
diagnostics, the multi-stage pipeline error wording, the CI recipe that bleep
itself uses for fast per-arch native-image validation, and a comparison table
against framework-native tagging.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Pass testTags to Project(...) in importer/generator call sites

The model.Project case class gained a `testTags: JsonMap[String, JsonSet[String]]`
field in 3d68163. The model file and the bleep.yaml-driven test projects
compiled clean locally because Zinc cached the importer/generator translation
units. CI cold-builds, surfacing four sites that construct Project explicitly:

- bleep-cli/src/scala/bleep/mavenimport/buildFromMavenPom.scala (main + test)
- bleep-cli/src/scala/bleep/mavenimport/generateBuildFromMaven.scala (scripts)
- bleep-cli/src/scala/bleep/sbtimport/generateBuild.scala (scripts)
- bleep-cli/src/scala/bleep/sbtimport/buildFromBloopFiles.scala (per-cross)

All four now pass `testTags = model.JsonMap.empty` in the correct position.
While here, type-annotate the bare `JsonSet.empty` calls in two of the same
files — without an inferrable expected type the Ordering instance was
ambiguous (Short vs Int both match Ordering[Any]) once shifted by the new
field's position.

* BuildCreateNew: pass testTags + type-annotate JsonSet.empty calls

Same fix as the importer/generator commit (8265f9d) for the two
`model.Project(...)` call sites in BuildCreateNew (empty + main proj).

* GenNativeImage: add -Ob quick-build for non-release builds

GraalVM's `-Ob` (alias `-O0`) skips advanced inlining / escape-analysis /
etc., trading runtime performance for 30-50% faster build. This is the
right trade for every non-release native-image invocation: PR / master
matrix runs (binary gets thrown away after the test step) and local
`bleep native-image` (testing, not benchmarking).

Release tag builds keep full `-O2` (default). Signal: `GITHUB_REF` env
var startswith `refs/tags/v`, same condition the `release` job in
build.yml uses to gate itself.

The chosen mode is logged at the top of each build so a reviewer can
confirm which mode produced a given artifact:

    native-image build mode [GITHUB_REF => refs/heads/master,
      mode => -Ob (quick build, snapshot/PR/local)]

Targets the macos-latest (arm64, 3 cpu / 7 GB) runner which was spending
~17 min in `native-image` proper. With -Ob that should drop closer to
~10-12 min; full job time from 35 min toward ~25.

Local: `bleep compile` clean, `bleep test` 793/793 pass.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant