diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cd03c2b..73efb29 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,18 +29,18 @@ jobs: with: persist-credentials: false - name: Setup Java - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: temurin java-version: 21 - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: languages: ${{ matrix.language }} queries: security-and-quality - name: Build for CodeQL run: mvn -B verify --file pom.xml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 70ef064..eb8c139 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -19,7 +19,7 @@ jobs: with: persist-credentials: false - name: Set up JDK - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: temurin java-version: 21 @@ -49,7 +49,7 @@ jobs: with: persist-credentials: false - name: Set up JDK - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: temurin java-version: 21 @@ -60,6 +60,9 @@ jobs: verify: name: Maven Verify (JDK ${{ matrix.java }}) runs-on: ubuntu-latest + permissions: + contents: read + id-token: write strategy: fail-fast: false matrix: @@ -71,7 +74,7 @@ jobs: with: persist-credentials: false - name: Set up JDK - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: temurin java-version: ${{ matrix.java }} @@ -99,5 +102,6 @@ jobs: uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: target/site/jacoco/jacoco.xml - fail_ci_if_error: false + use_oidc: true + fail_ci_if_error: true verbose: true diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a5e2ade..9ddc3a9 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -19,7 +19,7 @@ jobs: with: persist-credentials: false - name: Set up JDK - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: temurin java-version: 21 @@ -44,3 +44,27 @@ jobs: name: spotbugs-report path: target/spotbugsXml.xml if-no-files-found: ignore + + sonarcloud: + name: SonarCloud analysis + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + - name: Set up JDK + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + with: + distribution: temurin + java-version: 21 + cache: maven + - name: Build and analyze + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: >- + mvn -B verify sonar:sonar + -Dsonar.qualitygate.wait=true + -Dsonar.qualitygate.timeout=300 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3e7222c..cbb1823 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -9,8 +9,6 @@ on: - cron: "23 3 * * 1" workflow_dispatch: -permissions: read-all - jobs: scorecard: name: OSSF Scorecards @@ -36,7 +34,7 @@ jobs: results_format: sarif publish_results: true - name: Upload Scorecard SARIF - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: sarif_file: scorecard-results.sarif - name: Upload Scorecard artifact diff --git a/README.en.md b/README.en.md index 046316b..1f24f83 100644 --- a/README.en.md +++ b/README.en.md @@ -7,6 +7,7 @@ Curated Java reference implementations for deterministic algorithms and stateful [![CI](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/maven.yml/badge.svg)](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/maven.yml) [![Quality Gates](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/quality.yml/badge.svg)](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/quality.yml) [![CodeQL](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/codeql-analysis.yml) +[![SonarCloud Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=krotname_JavaAlgorithmsShowcase&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=krotname_JavaAlgorithmsShowcase) [![Coverage](https://codecov.io/gh/krotname/JavaAlgorithmsShowcase/branch/main/graph/badge.svg)](https://app.codecov.io/gh/krotname/JavaAlgorithmsShowcase) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/krotname/JavaAlgorithmsShowcase/badge)](https://securityscorecards.dev/viewer/?uri=github.com/krotname/JavaAlgorithmsShowcase) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13151/badge)](https://www.bestpractices.dev/projects/13151) @@ -21,8 +22,8 @@ Curated Java reference implementations for deterministic algorithms and stateful | Component | Engineering problem | What it demonstrates | | --- | --- | --- | -| [Transaction validation](src/main/java/transactions) | Ordered, stateful operations; cascading invalidation; balance and overflow boundaries | Deterministic state transitions, immutable results, integration scenarios | -| [Algorithm utilities](src/main/java/algorithms) | Boundary conditions, complexity, stream and file I/O | Deterministic APIs, explicit complexity tradeoffs, edge cases, and property-based invariants | +| [Transaction validation](src/main/java/transactions) | Ordered, stateful operations; cascading invalidation; balance and overflow boundaries | Deterministic state transitions, immutable results, integration and property-based tests | +| [Algorithm utilities](src/main/java/algorithms) | Boundary conditions, complexity, stream and file I/O | Deterministic APIs, explicit complexity tradeoffs, and edge cases | | [Parsing / validation](src/main/java/common/SafeParse.java) | Malformed numeric and text input | Explicit contracts, preserved exception causality, unit tests, and static analysis | This is an engineering demonstration repository, not a commercial product. Implementations are selected to make code quality, test strategy, and the development process directly reviewable. diff --git a/README.md b/README.md index 7528efe..dd7dcd7 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Curated Java reference implementations for deterministic algorithms and stateful [![CI](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/maven.yml/badge.svg)](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/maven.yml) [![Quality Gates](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/quality.yml/badge.svg)](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/quality.yml) [![CodeQL](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/krotname/JavaAlgorithmsShowcase/actions/workflows/codeql-analysis.yml) +[![SonarCloud Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=krotname_JavaAlgorithmsShowcase&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=krotname_JavaAlgorithmsShowcase) [![Coverage](https://codecov.io/gh/krotname/JavaAlgorithmsShowcase/branch/main/graph/badge.svg)](https://app.codecov.io/gh/krotname/JavaAlgorithmsShowcase) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/krotname/JavaAlgorithmsShowcase/badge)](https://securityscorecards.dev/viewer/?uri=github.com/krotname/JavaAlgorithmsShowcase) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13151/badge)](https://www.bestpractices.dev/projects/13151) @@ -21,8 +22,8 @@ Curated Java reference implementations for deterministic algorithms and stateful | Компонент | Инженерная задача | Что демонстрирует | | --- | --- | --- | -| [Transaction validation](src/main/java/transactions) | Порядок и состояние операций, каскадная невалидность, баланс и переполнение | Детерминированные переходы состояния, неизменяемый результат, интеграционные сценарии | -| [Algorithm utilities](src/main/java/algorithms) | Граничные условия, сложность, поточный и файловый ввод-вывод | Детерминированные API, явные компромиссы по сложности, edge cases и property-based инварианты | +| [Transaction validation](src/main/java/transactions) | Порядок и состояние операций, каскадная невалидность, баланс и переполнение | Детерминированные переходы состояния, неизменяемый результат, интеграционные и property-based тесты | +| [Algorithm utilities](src/main/java/algorithms) | Граничные условия, сложность, поточный и файловый ввод-вывод | Детерминированные API, явные компромиссы по сложности и edge cases | | [Parsing / validation](src/main/java/common/SafeParse.java) | Некорректный числовой и текстовый ввод | Явные контракты, сохранение причины исключения, unit-тесты и статический анализ | Это демонстрационный инженерный репозиторий, а не коммерческий продукт. Реализации отобраны так, чтобы на ревью были видны качество кода, тестовая стратегия и воспроизводимый процесс разработки. diff --git a/pom.xml b/pom.xml index f409930..3ef4590 100644 --- a/pom.xml +++ b/pom.xml @@ -20,13 +20,18 @@ 0.8.15 3.6.0 3.28.0 - 4.9.8.5 + 4.10.3.0 + 5.7.0.6970 UTF-8 - 6.1.1 + 6.1.2 3.27.7 1.10.1 - 6.1.1 + 6.1.2 1.18.46 + https://sonarcloud.io + krotname + krotname_JavaAlgorithmsShowcase + ${project.build.directory}/site/jacoco/jacoco.xml 0.70 0.70 0.70 @@ -233,6 +238,12 @@ + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar.maven.version} + diff --git a/src/main/java/interview/CallbackScheduler.java b/src/main/java/interview/CallbackScheduler.java index 5c8bd36..ad94f31 100644 --- a/src/main/java/interview/CallbackScheduler.java +++ b/src/main/java/interview/CallbackScheduler.java @@ -14,7 +14,7 @@ * Класс CallbackSchedulerImpl планирует исполнение Runnable в заданный момент времени. * Задачи делегируются ScheduledExecutorService, чтобы не держать отдельный ручной worker-loop. */ -public class CallbackScheduler implements AutoCloseable { +public final class CallbackScheduler implements AutoCloseable { private final ScheduledExecutorService scheduler; diff --git a/src/main/java/kyu6/TribonacciSequence.java b/src/main/java/kyu6/TribonacciSequence.java index 90d1868..a8749d1 100644 --- a/src/main/java/kyu6/TribonacciSequence.java +++ b/src/main/java/kyu6/TribonacciSequence.java @@ -4,19 +4,21 @@ public class TribonacciSequence { + private static final int SIGNATURE_SIZE = 3; + //6 https://www.codewars.com/kata/556deca17c58da83c00002db public static double[] tribonacci(double[] s, int n) { - if (s == null || s.length != 3) { + if (s == null || s.length != SIGNATURE_SIZE) { throw new IllegalArgumentException("signature must contain exactly three values"); } if (n < 0) { throw new IllegalArgumentException("sequence length must not be negative"); } double[] result = new double[n]; - int signatureLength = Math.min(s.length, n); + int signatureLength = Math.min(SIGNATURE_SIZE, n); System.arraycopy(s, 0, result, 0, signatureLength); - for (int i = s.length; i < n; i++) { + for (int i = SIGNATURE_SIZE; i < n; i++) { result[i] = result[i - 1] + result[i - 2] + result[i - 3]; } return result; diff --git a/src/main/java/transactions/ValidationResult.java b/src/main/java/transactions/ValidationResult.java index 95019a1..79c44df 100644 --- a/src/main/java/transactions/ValidationResult.java +++ b/src/main/java/transactions/ValidationResult.java @@ -1,18 +1,19 @@ package transactions; -import lombok.Getter; - +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -@Getter -final class ValidationResult { - private final List transactions; - - public ValidationResult(List transactions) { - this.transactions = List.copyOf(transactions); +record ValidationResult(List transactions) { + ValidationResult { + transactions = List.copyOf(transactions); } public static ValidationResult empty() { return new ValidationResult(List.of()); } + + public List getTransactions() { + return Collections.unmodifiableList(new ArrayList<>(transactions)); + } } diff --git a/src/test/java/quality/PropertySuite.java b/src/test/java/quality/PropertySuite.java index 5540edf..67c9b15 100644 --- a/src/test/java/quality/PropertySuite.java +++ b/src/test/java/quality/PropertySuite.java @@ -11,8 +11,8 @@ * to additional property-oriented classes when needed. */ @Suite -@SelectPackages({"kyu6", "kyu7"}) -@IncludeClassNamePatterns(".*(toCamelCaseTest|SumOddNumbersPropertyTest|ValidatePinPropertyTest)") +@SelectPackages({"kyu6", "kyu7", "transactions"}) +@IncludeClassNamePatterns(".*(toCamelCaseTest|SumOddNumbersPropertyTest|ValidatePinPropertyTest|InMemoryValidationServicePropertyTest)") @IncludeTags("property") public final class PropertySuite { private PropertySuite() { diff --git a/src/test/java/transactions/InMemoryValidationServicePropertyTest.java b/src/test/java/transactions/InMemoryValidationServicePropertyTest.java new file mode 100644 index 0000000..d90d805 --- /dev/null +++ b/src/test/java/transactions/InMemoryValidationServicePropertyTest.java @@ -0,0 +1,104 @@ +package transactions; + +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.Combinators; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; +import net.jqwik.api.Tag; +import net.jqwik.api.constraints.LongRange; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("property") +class InMemoryValidationServicePropertyTest { + private final ValidationService service = ValidationService.getInstance(); + + @Property(tries = 250) + void validationIsIndependentOfInputOrder( + @ForAll("transactionBatches") List transactions, + @ForAll @LongRange(min = 0, max = 1_000_000) long balance) { + List reversed = new ArrayList<>(transactions); + Collections.reverse(reversed); + + List directResult = service.validate(transactions, balance).getTransactions(); + List reversedResult = service.validate(reversed, balance).getTransactions(); + + assertEquals(directResult, reversedResult); + assertEquals(transactions.size(), directResult.size()); + } + + @Property(tries = 250) + void everyOrderHasOneConsistentStatus( + @ForAll("transactionBatches") List transactions, + @ForAll @LongRange(min = 0, max = 1_000_000) long balance) { + List statuses = service.validate(transactions, balance).getTransactions(); + Map statusByOrder = new HashMap<>(); + + for (int index = 0; index < statuses.size(); index++) { + TransactionStatus current = statuses.get(index); + if (index > 0) { + assertTrue(statuses.get(index - 1).transaction().id() < current.transaction().id()); + } + Boolean existing = statusByOrder.putIfAbsent(current.transaction().orderId(), current.status()); + if (existing != null) { + assertEquals(existing, current.status()); + } + } + } + + @Property(tries = 150) + void nonOverflowingWinsAreAlwaysAccepted( + @ForAll("winOnlyBatches") List transactions, + @ForAll @LongRange(min = 0, max = 1_000_000) long balance) { + List statuses = service.validate(transactions, balance).getTransactions(); + + assertEquals(transactions.size(), statuses.size()); + assertTrue(statuses.stream().allMatch(TransactionStatus::status)); + } + + @Provide + Arbitrary> transactionBatches() { + Arbitrary seed = Combinators.combine( + Arbitraries.longs().between(0, 10), + Arbitraries.longs().between(0, 10_000), + Arbitraries.of(TransactionType.values()) + ).as(TransactionSeed::new); + + return seed.list().ofMaxSize(30) + .map(InMemoryValidationServicePropertyTest::toTransactions); + } + + @Provide + Arbitrary> winOnlyBatches() { + return Arbitraries.longs().between(0, 10_000) + .list().ofMaxSize(30) + .map(amounts -> { + List transactions = new ArrayList<>(amounts.size()); + for (int index = 0; index < amounts.size(); index++) { + transactions.add(new Transaction(index + 1L, index % 7L, amounts.get(index), TransactionType.WIN)); + } + return transactions; + }); + } + + private static List toTransactions(List seeds) { + List transactions = new ArrayList<>(seeds.size()); + for (int index = 0; index < seeds.size(); index++) { + TransactionSeed seed = seeds.get(index); + transactions.add(new Transaction(index + 1L, seed.orderId(), seed.amount(), seed.type())); + } + return transactions; + } + + private record TransactionSeed(long orderId, long amount, TransactionType type) { + } +}