Skip to content

Commit 8845a51

Browse files
authored
Make quiet more quiet (#73)
1 parent f8e8cf3 commit 8845a51

3 files changed

Lines changed: 235 additions & 19 deletions

File tree

src/main/kotlin/file/SetupApp.kt

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ object SetupApp {
3232

3333
private data class WurstProcessResult(val exitCode: Int, val output: List<String>)
3434

35-
internal const val AGENTS_TEMPLATE_VERSION = "2026-06-10"
35+
internal const val AGENTS_TEMPLATE_VERSION = "2026-06-22"
3636
private const val AGENTS_TEMPLATE_MARKER_PREFIX = "<!-- WURST_AGENTS_TEMPLATE_VERSION:"
3737
private const val AGENTS_TEMPLATE_MARKER = "<!-- WURST_AGENTS_TEMPLATE_VERSION: $AGENTS_TEMPLATE_VERSION -->"
3838
private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes"
@@ -90,12 +90,16 @@ object SetupApp {
9090
}
9191

9292
private fun fail(message: String) {
93-
log.error(message)
93+
if (setup.quiet) {
94+
System.err.println(message)
95+
} else {
96+
log.error(message)
97+
}
9498
}
9599

96100
private fun detail(message: String) {
97101
if (setup.quiet) {
98-
println(message)
102+
System.err.println(message)
99103
} else {
100104
log.info(message)
101105
}
@@ -366,13 +370,15 @@ object SetupApp {
366370
if (printPjassFailure(result.output)) {
367371
return
368372
}
369-
fail("❌ Wurst $commandName failed.")
370-
detail("Exit code: ${result.exitCode}")
371373
if (setup.quiet) {
372-
detail("Next: rerun without `--quiet` only for the failed file/test.")
373-
} else {
374-
detail("Try: rerun with `--quiet` for a shorter error log, or `--debug` for troubleshooting details.")
374+
val diagnostics = quietCompilerFailureOutput(result.output, setup.debug)
375+
diagnostics.forEach { System.err.println(it) }
376+
fail("❌ Wurst $commandName failed. (Errors: ${quietCompilerErrorCount(result.output, diagnostics)})")
377+
return
375378
}
379+
fail("❌ Wurst $commandName failed.")
380+
detail("Exit code: ${result.exitCode}")
381+
detail("Try: rerun with `--quiet` for a shorter error log, or `--debug` for troubleshooting details.")
376382
}
377383

378384
private fun printPjassFailure(output: List<String>): Boolean {
@@ -402,14 +408,99 @@ object SetupApp {
402408
return true
403409
}
404410

405-
private fun isImportantCompilerLine(line: String): Boolean {
406-
return line.contains("error", ignoreCase = true) ||
407-
line.contains("warning", ignoreCase = true) ||
408-
line.contains("FAILED", ignoreCase = true) ||
411+
internal fun quietCompilerFailureOutput(output: List<String>, debug: Boolean): List<String> {
412+
return if (debug) output else quietCompilerDiagnostics(output)
413+
}
414+
415+
internal fun quietCompilerDiagnostics(output: List<String>): List<String> {
416+
val diagnostics = ArrayList<String>()
417+
var pendingVerboseError: MatchResult? = null
418+
var preservingTestFailureDetails = false
419+
420+
for (rawLine in output) {
421+
val line = rawLine.trimEnd()
422+
if (line.isBlank() || isNoisyCompilerVersionLine(line) || isQuietCompilerNoiseLine(line)) {
423+
continue
424+
}
425+
426+
val verboseError = Regex("""^Error in File (.+):(\d+):\s*$""").find(line.trim())
427+
if (verboseError != null) {
428+
pendingVerboseError = verboseError
429+
continue
430+
}
431+
432+
if (pendingVerboseError != null) {
433+
diagnostics.add(
434+
"Error ${pendingVerboseError.groupValues[1]}:${pendingVerboseError.groupValues[2]}: ${line.trim()}"
435+
)
436+
pendingVerboseError = null
437+
continue
438+
}
439+
440+
if (preservingTestFailureDetails && isQuietTestFailureDetailLine(line)) {
441+
diagnostics.add(line)
442+
continue
443+
}
444+
445+
if (isQuietCompilerDiagnosticLine(line)) {
446+
diagnostics.add(line)
447+
if (isQuietTestFailureHeader(line)) {
448+
preservingTestFailureDetails = true
449+
}
450+
}
451+
}
452+
453+
return diagnostics.distinct()
454+
}
455+
456+
internal fun quietCompilerErrorCount(
457+
output: List<String>,
458+
diagnostics: List<String> = quietCompilerDiagnostics(output)
459+
): Int {
460+
output.asSequence()
461+
.map { Regex("""^Errors:\s*(\d+)\s*$""").find(it.trim()) }
462+
.filterNotNull()
463+
.firstOrNull()
464+
?.let { return it.groupValues[1].toIntOrNull() ?: diagnostics.size.coerceAtLeast(1) }
465+
466+
return diagnostics.count {
467+
it.startsWith("Error ", ignoreCase = true) ||
468+
it.startsWith("FAILED ", ignoreCase = true) ||
469+
it.contains(" exception", ignoreCase = true) ||
470+
it.contains("Pjass", ignoreCase = true)
471+
}.coerceAtLeast(1)
472+
}
473+
474+
private fun isQuietCompilerDiagnosticLine(line: String): Boolean {
475+
return line.startsWith("Error ", ignoreCase = true) ||
476+
line.startsWith("FAILED ", ignoreCase = true) ||
477+
line.contains(" assertion", ignoreCase = true) ||
409478
line.contains("Exception", ignoreCase = true) ||
410479
line.contains("Pjass", ignoreCase = true)
411480
}
412481

482+
private fun isQuietTestFailureHeader(line: String): Boolean {
483+
return line.trim().equals("FAILED assertion:", ignoreCase = true)
484+
}
485+
486+
private fun isQuietTestFailureDetailLine(line: String): Boolean {
487+
val trimmed = line.trim()
488+
return trimmed.startsWith("Test failed:", ignoreCase = true) ||
489+
trimmed.contains(" inside call ", ignoreCase = true) ||
490+
trimmed.contains(" when calling ", ignoreCase = true)
491+
}
492+
493+
private fun isQuietCompilerNoiseLine(line: String): Boolean {
494+
val trimmed = line.trim()
495+
return trimmed.startsWith("Warning", ignoreCase = true) ||
496+
trimmed.matches(Regex("""^Errors:\s*\d+\s*$""")) ||
497+
trimmed.matches(Regex("""^Warnings:\s*\d+\s*$""")) ||
498+
trimmed.matches(Regex("""^Tests:\s*\d+/\d+\s+passed\s*$""", RegexOption.IGNORE_CASE)) ||
499+
trimmed.startsWith("compilation finished", ignoreCase = true) ||
500+
trimmed.startsWith("Running tests", ignoreCase = true) ||
501+
trimmed.startsWith("Finished running tests", ignoreCase = true)
502+
}
503+
413504
private fun isNoisyCompilerVersionLine(line: String): Boolean {
414505
val trimmed = line.trim()
415506
return trimmed == "Warning: Ignoring unknown wc3Patch in wurst.build: ${CoreJassProvider.DEFAULT_PATCH}" ||
@@ -960,11 +1051,6 @@ object SetupApp {
9601051
}
9611052
}
9621053
val exitCode = p.waitFor()
963-
if (setup.quiet && exitCode != 0) {
964-
val printableOutput = if (setup.debug) output else output.filterNot(::isNoisyCompilerVersionLine)
965-
val linesToPrint = if (compactFallback) printableOutput.filter(::isImportantCompilerLine) else printableOutput
966-
linesToPrint.forEach { println(it) }
967-
}
9681054
return WurstProcessResult(exitCode, output)
9691055
}
9701056

src/test/kotlin/GenerateTests.kt

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import file.SetupMain
88
import org.testng.Assert
99
import org.testng.annotations.Test
1010
import java.nio.file.Files
11+
import java.nio.file.Paths
1112
import java.util.Comparator
1213

1314
private class ExitException2(val code: Int) : RuntimeException("exit $code")
@@ -251,6 +252,12 @@ class GenerateTests {
251252
}
252253
@Test(priority = 10)
253254
fun testAgentsTemplateMarkerAndWarnings() {
255+
val templateFirstLine = Files.readAllLines(Paths.get("templates", "AGENTS.md")).first()
256+
Assert.assertEquals(
257+
templateFirstLine,
258+
"<!-- WURST_AGENTS_TEMPLATE_VERSION: ${SetupApp.AGENTS_TEMPLATE_VERSION} -->"
259+
)
260+
254261
val marked = SetupApp.withAgentsTemplateMarker("# AGENTS.md\n")
255262
Assert.assertTrue(marked.startsWith("<!-- WURST_AGENTS_TEMPLATE_VERSION: ${SetupApp.AGENTS_TEMPLATE_VERSION} -->"))
256263
Assert.assertNull(SetupApp.agentsTemplateWarning(marked))
@@ -278,6 +285,87 @@ class GenerateTests {
278285
Assert.assertTrue(setup.quiet)
279286
}
280287

288+
@Test(priority = 10)
289+
fun testQuietCompilerDiagnosticsSuppressGeneratedJassNoise() {
290+
val output = listOf(
291+
"Warnings: 3",
292+
"Warning: Error: e:Could not find variable silverGladeCounter.",
293+
"Warning: Error: e:Could not find a function with name eg",
294+
"Error Broken.wurst:12: Could not find variable realUserTypo.",
295+
"compilation finished (errors: 1, warnings: 3)",
296+
"Errors: 1"
297+
)
298+
299+
Assert.assertEquals(
300+
SetupApp.quietCompilerDiagnostics(output),
301+
listOf("Error Broken.wurst:12: Could not find variable realUserTypo.")
302+
)
303+
Assert.assertEquals(SetupApp.quietCompilerErrorCount(output), 1)
304+
}
305+
306+
@Test(priority = 10)
307+
fun testQuietCompilerDiagnosticsKeepFailedTestDetails() {
308+
val output = listOf(
309+
"Running tests",
310+
"Tests: 1/2 passed",
311+
"FAILED MyPkg.testExplodes",
312+
"\tFAILED assertion:",
313+
"\tTest failed: expected 1 but got 2",
314+
"\t ╚ MyTest.wurst:9 inside call assertEquals(1, 2)",
315+
"\t... when calling MyPkg.testExplodes(MyTest.wurst:12)",
316+
"Errors: 1",
317+
"Error MyTest.wurst:9: expected 1 but got 2",
318+
"Finished running tests"
319+
)
320+
321+
Assert.assertEquals(
322+
SetupApp.quietCompilerDiagnostics(output),
323+
listOf(
324+
"FAILED MyPkg.testExplodes",
325+
"\tFAILED assertion:",
326+
"\tTest failed: expected 1 but got 2",
327+
"\t ╚ MyTest.wurst:9 inside call assertEquals(1, 2)",
328+
"\t... when calling MyPkg.testExplodes(MyTest.wurst:12)",
329+
"Error MyTest.wurst:9: expected 1 but got 2"
330+
)
331+
)
332+
Assert.assertEquals(SetupApp.quietCompilerErrorCount(output), 1)
333+
}
334+
335+
@Test(priority = 10)
336+
fun testQuietCompilerDiagnosticsNormalizeVerboseFallbackErrors() {
337+
val output = listOf(
338+
"Error in File Broken.wurst:12:",
339+
" Could not find variable realUserTypo.",
340+
"Warning in File war3map.j:44:",
341+
" Error: e:Could not find variable silverGladeCounter."
342+
)
343+
344+
Assert.assertEquals(
345+
SetupApp.quietCompilerDiagnostics(output),
346+
listOf("Error Broken.wurst:12: Could not find variable realUserTypo.")
347+
)
348+
}
349+
350+
@Test(priority = 10)
351+
fun testQuietDebugCompilerFailureOutputBypassesFilter() {
352+
val output = listOf(
353+
"Error Broken.wurst:12: Could not find variable realUserTypo.",
354+
"java.lang.IllegalStateException: extra debug context",
355+
"\tat de.peeeq.wurstio.Main.main(Main.java:1)",
356+
"Warning: Error: e:Could not find variable generatedNoise."
357+
)
358+
359+
Assert.assertEquals(SetupApp.quietCompilerFailureOutput(output, debug = true), output)
360+
Assert.assertEquals(
361+
SetupApp.quietCompilerFailureOutput(output, debug = false),
362+
listOf(
363+
"Error Broken.wurst:12: Could not find variable realUserTypo.",
364+
"java.lang.IllegalStateException: extra debug context"
365+
)
366+
)
367+
}
368+
281369
@Test(priority = 10)
282370
fun testDevBuildFlag() {
283371
val setup = SetupMain()

templates/AGENTS.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- WURST_AGENTS_TEMPLATE_VERSION: 2026-06-10 -->
1+
<!-- WURST_AGENTS_TEMPLATE_VERSION: 2026-06-22 -->
22
# AGENTS.md - WurstScript Map Project Notes
33

44
WurstScript Warcraft III map project notes for editing `.wurst` code, dependencies, generated objects, tests, or map build logic.
@@ -8,11 +8,42 @@ WurstScript Warcraft III map project notes for editing `.wurst` code, dependenci
88
- Prefer simple, maintainable code. Fix root causes; avoid brittle workarounds, duplicated branches, and special-case patches.
99
- Keep packages focused and below ~500 lines; split by feature, responsibility, or data type.
1010
- Make changes in the source package, not generated output. Do not edit `_build/` or `_build/dependencies/` as source-of-truth.
11-
- Prefer Wurst standard-library wrappers and project helpers over raw `common.j`/Jass-style calls.
11+
- Use Wurst stdlib/library APIs and project helpers; never call a raw `common.j`/Jass native when a wrapper exists, and do not reinvent what stdlib already provides. See **Stdlib-First** below.
1212
- When unsure about Wurst syntax or local APIs, inspect nearby working code before guessing.
1313
- Keep tests narrow. Add/update tests for behavior, parsing, compiletime generation, or shared utilities.
1414
- Avoid broad refactors unless they directly reduce risk or complexity for the requested change.
1515

16+
## Stdlib-First: No Raw JASS Natives (Mandatory)
17+
18+
The most important coding rule: high-level Wurst packages must use the WurstScript stdlib and library APIs, never ported JASS. The goal is clean, reusable Wurst — not a JASS transliteration.
19+
20+
- Never call a raw `common.j` / `Blizzard.j` native when a Wurst wrapper or extension function exists. There is one for almost every native (on `unit`, `player`, `group`, `string`, `rect`, ...). Grep the stdlib (`_build/dependencies/wurstStdlib2/wurst/`) before writing a native call.
21+
- The only bar for a raw native is that you searched and confirmed no wrapper exists — then add a one-line comment saying so.
22+
- "It compiles" is not enough. Code that reads like JASS (manual handle juggling, native calls, global trigger callbacks, op-limit chunking) is wrong here; rewrite it idiomatically.
23+
24+
Use the stdlib API, not a raw native, for at least:
25+
26+
- Timers → `ClosureTimers` (`doAfter`, `doPeriodically`); never `CreateTimer`/`TimerStart`/`PauseTimer`/`DestroyTimer`.
27+
- Printing → `print` / `printTimed` / `p.print`; never `DisplayText*ToPlayer`/`...ToForce`.
28+
- Player state → `Player` extensions (`p.addGold`, `p.getGold`, `p.getId`, ...); prefer the `players[i]` array over `Player(i)`.
29+
- Unit inspection → `Unit` extensions (`u.getTypeId()`, `u.getOwner()`, `u.getAbilityLevel(id)`, ...).
30+
- Hashtables → `Hashtable` extensions (`ht.saveInt`/`loadInt`/`flushChild`/...).
31+
- Group iteration → `ClosureForGroups` (`forUnitsInRange`, `forUnitsInRect`) + `GroupUtils` (`getGroup()` / `group.release()`), not `GroupEnum*` + `ForGroup` globals.
32+
33+
The `CreateTrigger()..register...()..addAction() ->` cascade is the accepted idiom and is fine.
34+
35+
### Do Not Reinvent Stdlib Infrastructure (Mandatory)
36+
37+
Keep custom engine-level infrastructure to a minimum. Stdlib packages are battle-tested and handle the WC3 edge cases (recycling, op-limits, cleanup, desync) that hand-rolled versions get wrong. Grep for an existing system before building one; do not ship a parallel implementation of something stdlib provides:
38+
39+
- Dummy spell casting → `DummyCaster` / `InstantDummyCaster` (unit pooling: `DummyRecycler`).
40+
- Triggered damage → `DummyDamage` to deal, `DamageEvent` to detect/modify.
41+
- Events → `ClosureEvents` (`EventListener.add(...)`) / `RegisterEvents`; no custom global-trigger dispatcher or event bus.
42+
- Knockback / FX / sound / interpolation / orders → `Knockback3`, `Fx`, `SoundUtils`/`Sounds`, `Interpolation`, `Orders`/`OrderStringFactory`.
43+
- Collections → `LinkedList`, `HashMap`, `HashList`.
44+
45+
If stdlib almost fits, prefer a thin wrapper around the stdlib type over a from-scratch system and note why in a comment. Reinventing this is treated as a defect even if it compiles and passes tests, because it reintroduces solved bugs.
46+
1647
## Agent Workflow
1748

1849
Install dependencies:
@@ -43,6 +74,8 @@ For build changes:
4374
grill build ExampleMap.w3x --quiet
4475
```
4576

77+
Builds default to production mode, so compiletime `isProductionBuild()` returns `true`. Use `grill build ExampleMap.w3x --dev --quiet` only when validating run/development-mode behavior where `isProductionBuild()` must be `false`; `typecheck` and `test` do not need this flag.
78+
4679
Done means relevant errors/warnings are fixed or explicitly explained.
4780

4881
## Project Configuration
@@ -142,6 +175,15 @@ let label = count == 1 ? "unit" : "units"
142175

143176
These are recurring real-world Wurst/Warcraft III failure modes. Treat this section as a pre-edit checklist for any non-trivial Wurst change.
144177

178+
### Integer overflow
179+
180+
WC3 `int` is 32-bit signed and wraps silently at ~2.1 billion (`2^31 - 1`) — no exception, just a negative/garbage value that poisons every downstream comparison and division. Easy to hit when multiplying or summing large game quantities (gold/worth, army totals, damage products, accumulated stats).
181+
182+
- Promote to `real` BEFORE multiplying two large quantities: `a.toReal() * b`, never `(a * b).toReal()` (the latter already overflowed).
183+
- Same for running sums of products: `total += worth.toReal() * count * mult`.
184+
- Watch `worth * worth`, `count * worth`, `total * total` in scoring/stats; aggregate worths routinely exceed ~46k (the square root of int-max), so their product overflows in ordinary large games.
185+
- Wurst `/` is real division even for two ints (use `div` for integer division), so division itself does not overflow — but its operands still can. Prefer `real` for any accumulator that fans in many large terms.
186+
145187
### Closure capture is by value
146188

147189
Wurst closures capture locals by value. If a closure assigns to a local from an outer scope, the outer local is not updated.

0 commit comments

Comments
 (0)