diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a4ee2aa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Normalize all text files to LF in the repository. Working-tree endings are +# still the checkout platform's business; the stored blob is always LF. +* text=auto + +# The Unix start script must stay LF even on a Windows checkout, and the Windows +# script must be CRLF or cmd.exe mis-parses labels and multi-line set blocks. +/gradlew text eol=lf +*.bat text eol=crlf + +# Never run end-of-line conversion over binaries. +*.jar binary diff --git a/CHANGELOG.md b/CHANGELOG.md index 261a517..41f7869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,20 @@ grouped by date and milestone rather than semantic version. The content tracks the [readingbat-core](https://github.com/readingbat/readingbat-core) platform, so many entries reflect dependency and toolchain upgrades. -## [Unreleased] +## [1.1.0] - 2026-09-21 + +Test-reliability and dependency upkeep. No challenge content changed. ### Changed +- Bumped `version` in `gradle.properties` to `1.1.0`. + +- Upgraded dependencies: readingbat-core `3.3.1` → `3.4.0`, Ktor `3.5.1` → `3.6.0`, + Kotest `6.2.3` → `6.2.5`, kotlinter `5.6.0` → `5.7.0`, detekt `2.0.0-alpha.5` → + `2.0.0-alpha.6`, and the Gradle versions plugin `0.57.0` → `0.64.0`. Kotlin stays at + `2.4.10`, common-utils at `3.2.2`, and the JVM toolchain at **25**. + +- Upgraded the Gradle wrapper `9.6.1` → `9.7.1`. + - Split the `Test all challenges` case in `ContentTests` into per-language `Test all Java challenges` and `Test all Kotlin challenges`. `testApplication` wraps `runTest`, whose default timeout is 60s, and verifying every challenge in one body ran close enough to @@ -28,6 +39,15 @@ many entries reflect dependency and toolchain upgrades. - `Per-language tests cover every challenge` guard, so adding a language to `Content.kt` fails the suite rather than silently leaving its challenges untested. +- `.gitattributes`, which the repository had never had. `* text=auto` normalizes stored + blobs to LF regardless of a contributor's `core.autocrlf`, `*.jar` is pinned `binary`, + and the Gradle wrapper scripts get explicit endings: `/gradlew` stays `eol=lf` and + `*.bat` becomes `eol=crlf`. `gradlew.bat` had been committed with LF endings, which + `cmd.exe` mis-parses around labels and multi-line `set` blocks — CI is Linux-only, so + that would only ever have surfaced on a contributor's Windows checkout. `.editorconfig` + already declared `end_of_line = lf`, but that governs editors, not Git. + `git add --renormalize .` was a no-op, so no existing blob changed. + ## [1.0.1] - 2026-08-01 Documentation-only release. No content, dependency, or build-logic changes. diff --git a/CLAUDE.md b/CLAUDE.md index 8b59976..8a2c201 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,9 +7,12 @@ This is a **ReadingBat content repository** — it defines Java and Kotlin progr ## Conventions Record notable changes in `CHANGELOG.md` under an `[Unreleased]` heading; it becomes the -next version's section at release time. Releases are tagged without a `v` prefix (`1.0.1`), +next version's section at release time. Releases are tagged without a `v` prefix (`1.1.0`), and the version lives in `gradle.properties`. +`.gitattributes` governs line endings: stored blobs are LF, `gradlew` stays LF, and +`*.bat` is checked out CRLF because `cmd.exe` mis-parses LF batch files. + ## Build & Test Commands `make help` lists every build, test, lint, and run target. @@ -18,6 +21,21 @@ and the version lives in `gradle.properties`. ./gradlew test -Dkotest.filter.tests="" # Filter Kotest cases by name ``` +## Testing + +`src/test/kotlin/ContentTests.kt` sweeps every challenge through a Ktor test host, one +language at a time. Two constraints there are easy to break: + +- Await `runTestApplication` directly in the sweeps rather than calling `testApplication`. + `testApplication` is `runTestWithRealTime { runTestApplication(..) }`, and that wrapper + imposes `runTest`'s 60s default — a ceiling a Kotest `TestConfig(timeout = ..)` cannot + raise, which surfaces as `UncompletedCoroutinesError` on a slow runner instead of an + assertion failure. A Kotest test body is already a coroutine, so awaiting the inner + entry point leaves the declared timeout as the only governing limit. +- The sweeps name `content.java` and `content.kotlin` explicitly, so a language added to + `Content.kt` needs a sweep of its own. The `Per-language tests cover every challenge` + case fails the suite as a reminder. + ## Architecture ### Content Definition (DSL) diff --git a/README.md b/README.md index c1954ae..28400a3 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ src/main/kotlin//*.kt # Kotlin challenge files src/main/resources/application.conf # HOCON server config (content, site, Ktor settings) src/test/kotlin/ContentTests.kt # Kotest suite validating every challenge gradle/libs.versions.toml # Centralized dependency & toolchain versions +.gitattributes # Line-ending normalization (LF in-repo, CRLF for *.bat) ``` ## Tech Stack @@ -107,10 +108,19 @@ gradle/libs.versions.toml # Centralized dependency & toolchain versions ## Testing -`ContentTests.kt` uses Kotest `StringSpec` with Ktor's `testApplication` to validate -every challenge: empty answers must report `NOT_ANSWERED`, wrong answers `INCORRECT`, -and the expected outputs `CORRECT`. A CI workflow runs the tests and lint on every -push and pull request. +`ContentTests.kt` uses Kotest `StringSpec` to validate every challenge against a Ktor +test host: empty answers must report `NOT_ANSWERED`, wrong answers `INCORRECT`, and the +expected outputs `CORRECT`. + +The catalog is swept one language at a time — `Test all Java challenges` and `Test all +Kotlin challenges` — sharing a `verifyAllChallenges` helper, so a failure names the +language that broke. Both sweeps declare an explicit 5-minute timeout and await +`runTestApplication` directly; `testApplication` wraps it in `runTest`, whose 60-second +default a Kotest timeout cannot raise. Because those two cases name `content.java` and +`content.kotlin` explicitly, a `Per-language tests cover every challenge` guard fails the +suite if a language added to `Content.kt` is left uncovered. + +A CI workflow runs the tests and lint on JDK 25 for every push and pull request. ## Versioning diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a5a0848..7c44cb1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,6 +8,34 @@ changes, see [CHANGELOG.md](CHANGELOG.md). --- +## v1.1.0 (September 2026) + +A test-reliability release, prompted by a CI failure on a pull request that contained no +code at all. `ContentTests` verified the entire catalog in a single `Test all challenges` +body, and on a slow runner that sweep drifted past a timeout nobody had set deliberately. +Ktor's `testApplication` is `runTestWithRealTime { runTestApplication(..) }`, and the +wrapper imposes `runTest`'s 60-second default — a ceiling a Kotest timeout cannot raise, +so the build failed with `UncompletedCoroutinesError` rather than an assertion. + +The fix addresses both halves of the problem. The sweep is now split per language, so the +largest single body (Java, 11.4s locally) takes a little over half the time the combined +one did, and a failure names the language that broke rather than just "all challenges". +Both sweeps then await `runTestApplication` directly, which drops the hidden ceiling and +leaves an explicit 5-minute timeout as the only governing limit — confirmed by setting it +to one second and watching Kotest's own spec-scope timeout replace the coroutine error. +Because the two cases name `content.java` and `content.kotlin` explicitly, a new +`Per-language tests cover every challenge` guard fails the suite if a language added to +`Content.kt` would otherwise go silently untested. + +The release also adds the repository's first `.gitattributes`. `gradlew.bat` had been +stored with LF endings, which `cmd.exe` mis-parses around labels and multi-line `set` +blocks — a latent break that Linux-only CI would never have surfaced. Stored blobs are +now normalized to LF, `*.bat` is checked out CRLF, and `*.jar` is pinned binary. + +Dependencies moved forward as well: readingbat-core 3.4.0, Ktor 3.6.0, Kotest 6.2.5, +kotlinter 5.7.0, detekt 2.0.0-alpha.6, and the Gradle wrapper 9.7.1. No challenge content +changed in this release. + ## v1.0.1 (August 2026) A documentation correctness pass, with no content or build changes. Tagging `1.0.0` diff --git a/gradle.properties b/gradle.properties index 5a1c7c1..ef1db44 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.readingbat -version=1.0.1 +version=1.1.0 kotlin.code.style=official org.gradle.caching=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2552c8e..faccb44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] -detekt = "2.0.0-alpha.5" -gradle-wrapper = "9.6.1" +detekt = "2.0.0-alpha.6" +gradle-wrapper = "9.7.1" jvm = "25" -kotest = "6.2.3" +kotest = "6.2.5" kotlin = "2.4.10" -kotlinter = "5.6.0" -ktor = "3.5.1" +kotlinter = "5.7.0" +ktor = "3.6.0" logging = "8.0.4" -readingbat = "3.3.1" +readingbat = "3.4.0" utils = "3.2.2" -versions = "0.57.0" +versions = "0.64.0" [libraries] core-utils = { module = "com.pambrose.common-utils:core-utils", version.ref = "utils" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index b1b8ef5..eddabd2 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a9db115..ad7845b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/llms.txt b/llms.txt index f3dfc7c..3ede593 100644 --- a/llms.txt +++ b/llms.txt @@ -10,7 +10,8 @@ This repository provides interactive coding challenges served by [readingbat-cor - `src/main/kotlin/ContentServer.kt`: Server entry point (delegates to readingbat-core) - `src/main/java//*.java`: Java challenge files (each a class with a static method and main()) - `src/main/kotlin//*.kt`: Kotlin challenge files (top-level functions with main()) -- `src/test/kotlin/ContentTests.kt`: Kotest tests validating all challenges via Ktor test server +- `src/test/kotlin/ContentTests.kt`: Kotest suite validating all challenges via a Ktor test host +- `.gitattributes`: line-ending normalization — LF in the repository, `*.bat` checked out CRLF, `*.jar` binary ## Challenge Convention @@ -37,6 +38,14 @@ Run `make help` for the authoritative target list. - `make versions` — check for dependency updates - `./gradlew test -Dkotest.filter.tests=""` — filter Kotest cases by name +## Testing + +`ContentTests.kt` sweeps the catalog one language at a time (`Test all Java challenges`, `Test all Kotlin challenges`) through a shared `verifyAllChallenges` helper: empty answers must report `NOT_ANSWERED`, wrong answers `INCORRECT`, and each challenge's `main()` output `CORRECT`. + +Two constraints are easy to break when editing the suite: +- The sweeps await `runTestApplication` directly rather than calling `testApplication`. `testApplication` is `runTestWithRealTime { runTestApplication(..) }`, and that wrapper imposes `runTest`'s 60s default — a ceiling a Kotest `TestConfig(timeout = ..)` cannot raise. Awaiting the inner entry point leaves the declared 5-minute timeout as the only limit. +- The sweeps name `content.java` and `content.kotlin` explicitly, so a new language in `Content.kt` needs its own sweep. The `Per-language tests cover every challenge` case fails the suite as a reminder. + ## Versioning Releases are tagged (`1.0.0` onward) with the version tracked in `gradle.properties`. See `CHANGELOG.md` for the dated history and `RELEASE_NOTES.md` for narrative milestones.