Skip to content

Commit 0f5bc19

Browse files
authored
fix: harden kata boundaries and validation (#61)
* fix: harden kata boundaries and validation * ci: prime surefire before offline gate * ci: quote Maven selector on Windows
1 parent c48310d commit 0f5bc19

96 files changed

Lines changed: 1248 additions & 497 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/maven.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ permissions:
1010
contents: read
1111

1212
jobs:
13+
offline-gate:
14+
name: Offline gate (Windows, JDK 21)
15+
runs-on: windows-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
19+
with:
20+
persist-credentials: false
21+
- name: Set up JDK
22+
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
23+
with:
24+
distribution: temurin
25+
java-version: 21
26+
cache: maven
27+
- name: Prime the exact Maven test runtime
28+
run: mvn -B -ntp "-Dtest=quality.SmokeMethodTestHarnessTest" test --file pom.xml
29+
- name: Run the strict offline test gate
30+
shell: pwsh
31+
run: .\scripts\run-offline-gate.ps1 -Goal test -StrictDependencies
32+
1333
category-tests:
1434
name: Test (${{ matrix.category }})
1535
runs-on: ubuntu-latest

ARCHITECTURE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- `IntegrationSuite` – stateful transaction validation behavior.
1010
- `PropertySuite` – jqwik property/invariant tests.
1111
- `.github` contains CI, templates, and dependency automation.
12+
- `scripts/run-offline-gate.ps1` provides a network-free local Maven quality gate.
1213

1314
## Design principles used in this repo
1415

@@ -33,5 +34,5 @@
3334
## Future quality directions
3435

3536
- Increase property coverage into additional stable, pure utilities where invariant checks are natural.
36-
- Add explicit domain-level integration tests for edge-case transaction flows (duplicates / ordering / insufficient balance).
37+
- Expand domain-level integration coverage when new transaction rules are introduced.
3738
- Keep production methods with concise intent comments for non-obvious logic.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Corrected boundary, overflow, validation, determinism and complexity defects across kata utilities.
6+
- Hardened transaction validation and made returned status collections immutable.
7+
- Replaced unbounded iteration in encryption and range-counting helpers with logarithmic algorithms.
8+
- Made the generated smoke harness fail when every executable API path fails.
9+
- Added regression coverage for scheduler extremes, queueing, Fibonacci, trailing zeros and other edge cases.
10+
- Added a PowerShell offline quality gate with strict dependency-cache mode and a documented cached-JUnit fallback.
11+
- Added a Windows CI job that primes dependencies and then exercises the strict offline gate.
12+
- Expanded the verified test baseline to 594 tests.
13+
314
## 1.1 - public quality hardening (2026-06-09)
415

516
- Added CI, static analysis, and dependency management updates for repository quality.

README.en.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This repository is a portfolio-grade Java kata workspace focused on deterministi
3434
## CI and Quality Signals
3535

3636
- `.github/workflows/maven.yml` runs category jobs on JDK 21 and full `mvn verify` on JDK 17 and 21.
37+
- The same workflow validates the strict offline PowerShell gate on Windows after an explicit dependency-prime step.
3738
- `.github/workflows/quality.yml` runs Checkstyle, PMD, and SpotBugs without executing tests.
3839
- `.github/workflows/codeql-analysis.yml` runs security analysis.
3940
- `.github/dependabot.yml` keeps dependency automation visible.
@@ -58,6 +59,15 @@ mvn -B test -Dtest='quality.IntegrationSuite'
5859
mvn -B test -Dtest='quality.PropertySuite'
5960
```
6061

62+
Run the complete PowerShell gate without network access:
63+
64+
```powershell
65+
.\scripts\run-offline-gate.ps1
66+
```
67+
68+
See [`TESTING.md`](TESTING.md) for strict dependency-cache mode and the documented
69+
cached-JUnit fallback.
70+
6171
## Reviewer Checklist
6272

6373
- No hidden mutation of test inputs inside algorithmic methods.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
- **Coverage-gate**: JaCoCo ломает `mvn verify`, если покрытие ниже 70% строк, ветвлений
4747
или инструкций.
4848
- **Отдельный quality-гейт**: workflow `.github/workflows/quality.yml` для чистых статических проверок без прогона тестов.
49+
- **Офлайн-гейт**: Windows CI после явного заполнения кэша запускает PowerShell-проверку в строгом режиме без сети.
4950
- **Checkstyle / PMD / SpotBugs**: статические проверки заведены в quality-гейт.
5051
- **Checkstyle**: проектный набор правил в `config/checkstyle/checkstyle.xml` и явные suppressions для generated smoke-тестов.
5152
- **SpotBugs**: чистый отчёт с явными исключениями совместимости в `config/spotbugs-exclude.xml`.
@@ -63,6 +64,15 @@ mvn -B test -Dtest='quality.IntegrationSuite'
6364
mvn -B test -Dtest='quality.PropertySuite'
6465
```
6566

67+
Полный локальный прогон без сети на PowerShell:
68+
69+
```powershell
70+
.\scripts\run-offline-gate.ps1
71+
```
72+
73+
Подробности о строгом режиме зависимостей и fallback на уже закэшированную версию JUnit
74+
описаны в [`TESTING.md`](TESTING.md).
75+
6676
### Что отражает репозиторий как "публично привлекательный"
6777

6878
- Есть CI с понятными этапами и артефактами (`surefire`, `jacoco`).

TESTING.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,36 @@ mvn -B test -Dtest='quality.PropertySuite'
6464
- line coverage: 70%
6565
- branch coverage: 70%
6666
- instruction coverage: 70%
67-
- Current verified local baseline: 455 tests, 91.4% line coverage,
68-
85.1% branch coverage and 92.7% instruction coverage.
67+
- Current verified local baseline: 594 tests. Coverage is enforced by the ratios above;
68+
the generated HTML/XML report under `target/site/jacoco` is the source of exact values.
6969
- Run:
7070

7171
```bash
7272
mvn -B verify
7373
```
7474

75+
### Offline gate (PowerShell)
76+
77+
The repository also provides a network-free local gate:
78+
79+
```powershell
80+
.\scripts\run-offline-gate.ps1
81+
.\scripts\run-offline-gate.ps1 -Goal test
82+
.\scripts\run-offline-gate.ps1 -StrictDependencies
83+
```
84+
85+
The script always passes Maven `--offline`. By default, when the exact JUnit BOM from
86+
`pom.xml` is not present locally, it warns and uses the newest cached release from the
87+
same major line for validation. `-StrictDependencies` disables that fallback and fails
88+
unless the exact dependency set is already cached.
89+
7590
- CI uploads `target/surefire-reports` and `target/site/jacoco`.
7691

7792
## 6) CI matrix
7893

7994
- Category workflow: smoke/integration/property tags (JDK 21)
8095
- Full verify workflow: JDK 17 and 21
96+
- Strict offline PowerShell gate: Windows, JDK 21 (after an explicit cache-prime step)
8197
- Static workflow: static checks only (JDK 21)
8298

8399
## 7) UI category status

scripts/run-offline-gate.ps1

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
[CmdletBinding()]
2+
param(
3+
[ValidateSet('test', 'verify')]
4+
[string] $Goal = 'verify',
5+
6+
[switch] $StrictDependencies
7+
)
8+
9+
Set-StrictMode -Version Latest
10+
$ErrorActionPreference = 'Stop'
11+
12+
$repoRoot = Split-Path -Parent $PSScriptRoot
13+
$pomPath = Join-Path $repoRoot 'pom.xml'
14+
$maven = Get-Command mvn -ErrorAction Stop
15+
$localRepository = if ($env:MAVEN_REPO_LOCAL) {
16+
$env:MAVEN_REPO_LOCAL
17+
} else {
18+
Join-Path $env:USERPROFILE '.m2\repository'
19+
}
20+
21+
[xml] $pom = Get-Content -LiteralPath $pomPath -Raw
22+
$configuredJUnit = [string] $pom.project.properties.'junit.version'
23+
$configuredPlatform = [string] $pom.project.properties.'junit.platform.version'
24+
$configuredBom = Join-Path $localRepository (
25+
'org\junit\junit-bom\{0}\junit-bom-{0}.pom' -f $configuredJUnit
26+
)
27+
28+
$mavenArguments = @(
29+
'--offline'
30+
'--batch-mode'
31+
'--no-transfer-progress'
32+
'--file'
33+
$pomPath
34+
)
35+
36+
if (-not (Test-Path -LiteralPath $configuredBom)) {
37+
if ($StrictDependencies) {
38+
throw "The configured JUnit BOM $configuredJUnit is absent from the local Maven cache."
39+
}
40+
41+
$configuredMajor = ([version] $configuredJUnit).Major
42+
$bomRoot = Join-Path $localRepository 'org\junit\junit-bom'
43+
$fallback = Get-ChildItem -LiteralPath $bomRoot -Directory -ErrorAction SilentlyContinue |
44+
Where-Object {
45+
$_.Name -match '^\d+\.\d+\.\d+$' -and
46+
([version] $_.Name).Major -eq $configuredMajor -and
47+
(Test-Path -LiteralPath (Join-Path $_.FullName ("junit-bom-{0}.pom" -f $_.Name)))
48+
} |
49+
Sort-Object { [version] $_.Name } -Descending |
50+
Select-Object -First 1
51+
52+
if ($null -eq $fallback) {
53+
throw "No JUnit $configuredMajor.x BOM is available in the local Maven cache."
54+
}
55+
56+
Write-Warning ((
57+
"JUnit BOM $configuredJUnit is not cached; offline validation uses cached {0}. " +
58+
"Run with -StrictDependencies to require the exact POM version."
59+
) -f $fallback.Name)
60+
$mavenArguments += "-Djunit.version=$($fallback.Name)"
61+
62+
if ($configuredPlatform -eq $configuredJUnit) {
63+
$mavenArguments += "-Djunit.platform.version=$($fallback.Name)"
64+
}
65+
}
66+
67+
Push-Location $repoRoot
68+
try {
69+
& $maven.Source @mavenArguments $Goal
70+
if ($LASTEXITCODE -ne 0) {
71+
throw "Offline Maven gate failed with exit code $LASTEXITCODE."
72+
}
73+
} finally {
74+
Pop-Location
75+
}

src/main/java/interview/CallbackScheduler.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,23 @@ public CallbackScheduler() {
3333
public void schedule(Runnable callback, Instant when) {
3434
Objects.requireNonNull(callback);
3535
Objects.requireNonNull(when);
36-
long delayMillis = Math.max(0L, Duration.between(Instant.now(), when).toMillis());
36+
long delayMillis = delayMillisUntil(when);
3737
scheduler.schedule(callback, delayMillis, TimeUnit.MILLISECONDS);
3838
}
3939

40+
private static long delayMillisUntil(Instant when) {
41+
Duration delay = Duration.between(Instant.now(), when);
42+
if (delay.isNegative() || delay.isZero()) {
43+
return 0L;
44+
}
45+
try {
46+
return delay.toMillis();
47+
} catch (ArithmeticException ignored) {
48+
// Instant supports a wider range than a millisecond delay stored in a long.
49+
return Long.MAX_VALUE;
50+
}
51+
}
52+
4053
@Override
4154
public void close() {
4255
scheduler.shutdownNow();

src/main/java/kyu4/MorseCodeDecoder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.ArrayList;
55
import java.util.LinkedHashMap;
66
import java.util.List;
7+
import java.util.Locale;
78
import java.util.Map;
89
import java.util.StringJoiner;
910

@@ -129,7 +130,7 @@ public static String decodeMorse(String morseCode) {
129130
}
130131
decodedWords.add(decodedWord.toString());
131132
}
132-
return String.join(" ", decodedWords).toUpperCase();
133+
return String.join(" ", decodedWords).toUpperCase(Locale.ROOT);
133134
}
134135

135136
public static String encode(String toMorseCode) {
@@ -141,7 +142,7 @@ public static String encode(String toMorseCode) {
141142
for (String word : toMorseCode.trim().split("\\s+")) {
142143
StringJoiner letters = new StringJoiner(" ");
143144
for (char letter : word.toCharArray()) {
144-
String morse = ALPHABET_TO_MORSE.get(String.valueOf(letter).toLowerCase());
145+
String morse = ALPHABET_TO_MORSE.get(String.valueOf(letter).toLowerCase(Locale.ROOT));
145146
if (morse == null) {
146147
throw new IllegalArgumentException("Unsupported Morse character: " + letter);
147148
}

src/main/java/kyu5/HumanReadableTime.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public class HumanReadableTime {
2121
private static final int SIXTY = 60;
2222

2323
public static String makeReadable(int seconds) {
24-
if (seconds > 359999) {
25-
throw new IllegalArgumentException("very long arguments");
24+
if (seconds < 0 || seconds > 359999) {
25+
throw new IllegalArgumentException("seconds must be between 0 and 359999");
2626
}
2727
int HH = 0;
2828
int MM = 0;

0 commit comments

Comments
 (0)