diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..764b14a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,89 @@ +# SPA2 CI — migration contract (issue abandonrules/sifry#8) +# +# Every push/PR validates the Phase-1 tiered test suite: +# Tier 1+2 (JVM + Robolectric unit tests) run here on linux. +# Tier 3 (instrumented, NDK/PCRE on-device) intentionally stays on the +# physical Pixel locally (serial 52141FDAS001QF) — see README "Testing". +# +# Toolchain tracked here: JDK 17 (temurin), platform android-35, +# build-tools 36.1.0, CMake 3.22.1, NDK 27.0.12077973, Gradle 9.7.1 +# (wrapper). Robolectric 4.17-beta-4 is the AGP/JDK-sensitive pin that must +# advance with the migration (see issue notes). + +name: build + +on: + push: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build-and-unit-test: + name: Assemble + JVM/Robolectric tests (JDK ${{ matrix.jdk }}) + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + jdk: ['17'] + env: + ANDROID_HOME: /usr/local/lib/android/sdk + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.jdk }} + + - name: Set up Gradle (wrapper + dependency cache) + uses: gradle/actions/setup-gradle@v4 + + - name: Cache Android NDK and CMake + uses: actions/cache@v4 + with: + path: | + ${{ env.ANDROID_HOME }}/ndk/27.0.12077973 + ${{ env.ANDROID_HOME }}/cmake/3.22.1 + key: android-${{ runner.os }}-ndk27-cmake3221 + + - name: Accept Android SDK licenses + run: | + SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" + if [ ! -x "$SDKMANAGER" ]; then + echo "sdkmanager not found at $SDKMANAGER" >&2 + exit 1 + fi + yes | "$SDKMANAGER" --licenses > /dev/null + + - name: Install Android SDK packages + run: | + SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" + "$SDKMANAGER" \ + "platforms;android-35" \ + "build-tools;36.1.0" \ + "ndk;27.0.12077973" \ + "cmake;3.22.1" + + - name: Tier 1+2 — JVM + Robolectric unit tests + run: ./gradlew :spa2:testDebugUnitTest + + - name: Assemble debug APK (incl. native libregrep) + run: ./gradlew :spa2:assembleDebug + + - name: Upload unit test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: unit-test-reports-jdk${{ matrix.jdk }} + path: | + spa2/build/reports/tests/* + spa2/build/test-results/**/TEST-*.xml + if-no-files-found: ignore \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..95ea4f7 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# SPA2 + +SPA2 is a self-hosted collection of cipher and code tools for Android +(substitution ciphers, Morse code, Braille, and more). Native UI in Java with +a C/C++ core (PCRE-based search). + +## Building + +Requirements: +- JDK 17+ (build validated on JDK 17, 24 and 26) +- Android SDK with: platform android-35, build-tools 36.1.0, CMake 3.22.1, + NDK 27.0.12077973 + +Point Gradle at your SDK one of these ways: + +1. Set `ANDROID_HOME` (recommended, no repo changes needed): + + ```sh + export ANDROID_HOME=$HOME/Android/Sdk + ./gradlew :spa2:assembleDebug + ``` + +2. Or create `local.properties` in the repo root (see + `local.properties.example`): + + ```properties + sdk.dir=/path/to/Android/Sdk + ``` + +`local.properties` is machine-specific and intentionally not tracked. + +The output APK is `spa2/build/outputs/apk/debug/spa2-debug.apk`. + +## Testing + +The migration relies on a 3-tier test suite (the "migration contract", +issue #7). Tiers 1 and 2 run on any JVM anywhere; Tier 3 needs a device. + +| Tier | Where it runs | Command | +|---|---|---| +| 1. Pure JVM unit tests | CI + local | `./gradlew :spa2:testDebugUnitTest` | +| 2. Robolectric (resources/prefs/XML) | CI + local | `./gradlew :spa2:testDebugUnitTest` | +| 3. Instrumented (native `libregrep`/PCRE, app smoke) | **physical Pixel, local only** | `./gradlew :spa2:connectedDebugAndroidTest` | + +Tier 3 — the NDK/PCRE safety net (`RegExpNativeTest` + `AppSmokeTest`) — +intentionally stays on the attached Pixel (`adb -s 52141FDAS001QF`): +GitHub Actions runners cannot host physical devices, and Managed/Firebase +Test Lab is deferred (issue #8). When a step touches native code or JNI, +run Tier 3 locally before closing the issue. + +## CI + +`.github/workflows/build.yml` runs Tier 1+2 and `assembleDebug` on every +push/PR (JDK 17, platform android-35, build-tools 36.1.0, CMake 3.22.1, +NDK 27.0.12077973, Gradle 9.7.1 from the wrapper). Robolectric is pinned to +4.17-beta-4 — an AGP/JDK-sensitive choice that must advance with the +toolchain (unit tests were validated on JDK 17/26). + +## Versioning + +`versionCode`/`versionName` live in `spa2/src/main/AndroidManifest.xml`. +Codes use the scheme `major*10000 + minor*1000 + patch*10 + sub`, so +4.0.0 = 40000. Codes only ever increase (Android requires it for sideloaded +upgrades). \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8c40a77..812b76a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { -id 'com.android.application' version '8.7.1' apply false +id 'com.android.application' version '9.4.0' apply false } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 92b9117..3e927b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,5 +18,4 @@ android.useAndroidX=true # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true -android.enableJetifier=true \ No newline at end of file +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0..c42672d 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-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/local.properties.example b/local.properties.example new file mode 100644 index 0000000..02e2c73 --- /dev/null +++ b/local.properties.example @@ -0,0 +1,3 @@ +# Copy to local.properties and set your SDK path. +# Gradle also honors the ANDROID_HOME environment variable instead. +sdk.dir=/path/to/Android/Sdk \ No newline at end of file diff --git a/spa2/build.gradle b/spa2/build.gradle index a615c33..8e959da 100644 --- a/spa2/build.gradle +++ b/spa2/build.gradle @@ -3,13 +3,14 @@ plugins { } android { - namespace 'cz.absolutno.sifry' - compileSdk 35 + namespace = 'cz.absolutno.sifry' + compileSdk = 35 defaultConfig { - applicationId 'cz.absolutno.sifry' - minSdk 21 - targetSdk 35 + applicationId = 'cz.absolutno.sifry' + minSdk = 21 + targetSdk = 35 + testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner' externalNativeBuild { cmake { arguments '-DANDROID_STL=c++_shared' @@ -19,19 +20,37 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled = false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } externalNativeBuild { cmake { - version '3.22.1' - path 'src/main/cpp/CMakeLists.txt' + version = '3.22.1' + path = 'src/main/cpp/CMakeLists.txt' + } + } + + buildToolsVersion = '36.1.0' + ndkVersion = '27.0.12077973' + + packagingOptions { + jniLibs { + useLegacyPackaging = false + } + } + + testOptions { + unitTests { + includeAndroidResources = true + all { + maxHeapSize = '2g' + } } } } @@ -39,4 +58,11 @@ android { dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.preference:preference:1.2.1' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.17-beta-4' + + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test:core:1.6.1' } \ No newline at end of file diff --git a/spa2/src/androidTest/java/cz/absolutno/sifry/AppSmokeTest.java b/spa2/src/androidTest/java/cz/absolutno/sifry/AppSmokeTest.java new file mode 100644 index 0000000..ca1b95c --- /dev/null +++ b/spa2/src/androidTest/java/cz/absolutno/sifry/AppSmokeTest.java @@ -0,0 +1,85 @@ +package cz.absolutno.sifry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; + +import androidx.lifecycle.Lifecycle; +import androidx.test.core.app.ActivityScenario; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import cz.absolutno.sifry.braille.BrailleActivity; +import cz.absolutno.sifry.cisla.CislaActivity; +import cz.absolutno.sifry.frekvence.FrekvActivity; +import cz.absolutno.sifry.kalendar.KalendarActivity; +import cz.absolutno.sifry.mainscreen.MainActivity; +import cz.absolutno.sifry.mainscreen.SplashActivity; +import cz.absolutno.sifry.morse.MorseActivity; +import cz.absolutno.sifry.regexp.RegExpActivity; +import cz.absolutno.sifry.semafor.SemaforActivity; +import cz.absolutno.sifry.substituce.SubstActivity; +import cz.absolutno.sifry.tabulky.TabulkyActivity; +import cz.absolutno.sifry.transpozice.TransActivity; +import cz.absolutno.sifry.vlajky.VlajkyActivity; +import cz.absolutno.sifry.zapisnik.ZapisnikActivity; + +@RunWith(AndroidJUnit4.class) +public class AppSmokeTest { + + private static final Class[] CIPHER_ACTIVITIES = { + MorseActivity.class, + BrailleActivity.class, + CislaActivity.class, + SemaforActivity.class, + TabulkyActivity.class, + VlajkyActivity.class, + SubstActivity.class, + TransActivity.class, + FrekvActivity.class, + KalendarActivity.class, + ZapisnikActivity.class, + RegExpActivity.class, + }; + + @Test + public void splashToMain() throws Exception { + Instrumentation inst = InstrumentationRegistry.getInstrumentation(); + Context ctx = ApplicationProvider.getApplicationContext(); + Instrumentation.ActivityMonitor monitor = + inst.addMonitor(MainActivity.class.getName(), null, false); + try { + Intent splashIntent = new Intent(ctx, SplashActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Activity splash = inst.startActivitySync(splashIntent); + assertEquals("SplashActivity did not resume", "mainscreen.SplashActivity", + splash.getLocalClassName()); + Activity main = inst.waitForMonitorWithTimeout(monitor, 15000); + assertNotNull("MainActivity did not open after SplashActivity", main); + main.finish(); + splash.finish(); + } finally { + inst.removeMonitor(monitor); + } + } + + @Test + public void eachCipherActivityRenders() { + Context ctx = ApplicationProvider.getApplicationContext(); + for (Class cls : CIPHER_ACTIVITIES) { + try (ActivityScenario scenario = ActivityScenario.launch(new Intent(ctx, cls))) { + scenario.moveToState(Lifecycle.State.RESUMED); + assertEquals(cls.getSimpleName() + " did not reach RESUMED", + Lifecycle.State.RESUMED, scenario.getState()); + } + } + } +} \ No newline at end of file diff --git a/spa2/src/androidTest/java/cz/absolutno/sifry/regexp/RegExpNativeTest.java b/spa2/src/androidTest/java/cz/absolutno/sifry/regexp/RegExpNativeTest.java new file mode 100644 index 0000000..4bac816 --- /dev/null +++ b/spa2/src/androidTest/java/cz/absolutno/sifry/regexp/RegExpNativeTest.java @@ -0,0 +1,97 @@ +package cz.absolutno.sifry.regexp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.res.AssetManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class RegExpNativeTest { + + private static RegExpNative.Report waitFor(RegExpNative rn, long timeoutMs) + throws InterruptedException { + long start = System.currentTimeMillis(); + RegExpNative.Report rep; + do { + rep = rn.getProgress(); + if (!rep.running) + return rep; + Thread.sleep(50); + } while (System.currentTimeMillis() - start < timeoutMs); + return rep; + } + + private static AssetManager assets() { + return ApplicationProvider.getApplicationContext().getAssets(); + } + + @Test + public void matchesWord() throws Exception { + RegExpNative rn = new RegExpNative(); + try { + rn.startThread(assets(), "raw/en.canon", new String[]{"^aardvark:"}); + RegExpNative.Report rep = waitFor(rn, 30000); + assertFalse("regexp search timed out", rep.running); + assertFalse("error: " + rn.getError(), rep.error); + assertTrue("no matches for ^aardvark: (matches=" + rep.matches + ")", rep.matches >= 1); + assertEquals("aardvark", rn.getResult(0)); + } finally { + rn.free(); + } + } + + @Test + public void switchDictionaryReplacesResults() throws Exception { + RegExpNative rn = new RegExpNative(); + try { + rn.startThread(assets(), "raw/en.canon", new String[]{"^aardvark:"}); + RegExpNative.Report rep = waitFor(rn, 30000); + assertFalse("regexp search timed out", rep.running); + assertFalse("error: " + rn.getError(), rep.error); + assertEquals("en results: (matches=" + rep.matches + ")", 1, rep.matches); + assertEquals("aardvark", rn.getResult(0)); + + rn.startThread(assets(), "raw/cs.canon", new String[]{"^sifry:"}); + rep = waitFor(rn, 60000); + assertFalse("regexp search timed out", rep.running); + assertFalse("cs error: " + rn.getError(), rep.error); + assertTrue("cs switch lost matches (matches=" + rep.matches + ")", rep.matches >= 2); + assertEquals("šifry", rn.getResult(0)); + rn.free(); + } + } + + @Test + public void noMatches() throws Exception { + RegExpNative rn = new RegExpNative(); + try { + rn.startThread(assets(), "raw/en.canon", new String[]{"^zzzzqqq:"}); + RegExpNative.Report rep = waitFor(rn, 30000); + assertFalse("error: " + rn.getError(), rep.error); + assertEquals(0, rep.matches); + } finally { + rn.free(); + } + } + + @Test + public void missingAssetFails() throws Exception { + RegExpNative rn = new RegExpNative(); + try { + rn.startThread(assets(), "raw/no-such.gz", new String[]{"^x:"}); + RegExpNative.Report rep = rn.getProgress(); + assertTrue(rep.error); + assertFalse(rep.running); + assertTrue(rn.getError().contains("Can't open asset")); + } finally { + rn.free(); + } + } +} \ No newline at end of file diff --git a/spa2/src/main/AndroidManifest.xml b/spa2/src/main/AndroidManifest.xml index b99b0d9..869776c 100644 --- a/spa2/src/main/AndroidManifest.xml +++ b/spa2/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="40000" + android:versionName="4.0.0"> && patterns) { free(); try { + { + std::lock_guard lock{progressMutex}; + running = true; + } worker = std::thread{threadMain, this, std::move(asset), std::move(patterns)}; } catch(const std::system_error&) { std::lock_guard lock{progressMutex}; error = "Unable to start thread"; + running = false; } } diff --git a/spa2/src/test/java/cz/absolutno/sifry/InfraSmokeTest.java b/spa2/src/test/java/cz/absolutno/sifry/InfraSmokeTest.java new file mode 100644 index 0000000..0154637 --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/InfraSmokeTest.java @@ -0,0 +1,35 @@ +package cz.absolutno.sifry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Arrays; + +import cz.absolutno.sifry.morse.MorseDecoder; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 35, qualifiers = "en-rUS") +public class InfraSmokeTest { + + @Test + public void appInstanceIsInitialized() { + assertNotNull(App.getContext()); + assertSame(App.getContext(), RuntimeEnvironment.getApplication()); + } + + @Test + public void morseDecodesFromResources() { + MorseDecoder md = new MorseDecoder(); + assertEquals("A", md.decode(5)); + ArrayList list = new ArrayList<>(Arrays.asList(5, 24)); + assertEquals("AB", md.decode(list)); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/cisla/CislaConvTest.java b/spa2/src/test/java/cz/absolutno/sifry/cisla/CislaConvTest.java new file mode 100644 index 0000000..989e575 --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/cisla/CislaConvTest.java @@ -0,0 +1,63 @@ +package cz.absolutno.sifry.cisla; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import cz.absolutno.sifry.R; + +public class CislaConvTest { + + @Test + public void permParseEmpty() { + assertEquals(0, CislaConv.parsePerm("", R.id.idCDPerm1)); + } + + @Test + public void permMissesGray() { + assertEquals(1, CislaConv.parsePerm("ABCD", R.id.idCDPerm1)); + } + + @Test + public void permRoundtripAll() { + for (int x = 0; x <= 23; x++) { + assertEquals(x + 1, CislaConv.parsePerm(CislaConv.toPerm(x), R.id.idCDPerm1)); + } + } + + @Test + public void permToKnownSentinel() { + assertEquals("ABCD", CislaConv.toPerm(0)); + assertEquals("DCBA", CislaConv.toPerm(23)); + } + + @Test + public void romanParseKnown() { + assertEquals(1984, CislaConv.parseRoman("MCMLXXXIV")); + } + + @Test + public void romanRejectsIncorrect() { + assertEquals(-1, CislaConv.parseRoman("IC")); + assertEquals(-1, CislaConv.parseRoman("ABC")); + } + + @Test + public void romanRoundtripSmall() { + for (int x = 1; x <= 39; x++) { + assertEquals(x, CislaConv.parseRoman(CislaConv.toRoman(x))); + } + } + + @Test + public void romanZero() { + assertEquals("\u2013\u2013\u2013\u2013", CislaConv.toRoman(0)); + } + + @Test + public void binaryTernary() { + assertEquals("00001101", CislaConv.toBinary(13, 8)); + assertEquals("111", CislaConv.toTernary(13)); + assertEquals("100", CislaConv.toTernary(9)); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/common/alphabet/AlphabetTest.java b/spa2/src/test/java/cz/absolutno/sifry/common/alphabet/AlphabetTest.java new file mode 100644 index 0000000..fcd162b --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/common/alphabet/AlphabetTest.java @@ -0,0 +1,165 @@ +package cz.absolutno.sifry.common.alphabet; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Locale; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class AlphabetTest { + + private Locale saved; + + @Before + public void saveLocale() { + saved = Locale.getDefault(); + } + + @After + public void restoreLocale() { + Locale.setDefault(saved); + } + + private static ArrayList ords(StringParser sp) { + ArrayList out = new ArrayList<>(); + int o; + while ((o = sp.getNextOrd()) != StringParser.EOF) + out.add(o); + return out; + } + + @Test + public void englishDefault26() { + Locale.setDefault(Locale.ENGLISH); + Alphabet a = Alphabet.getVariantInstance(26, ""); + assertEquals(26, a.count()); + assertEquals("Z", a.chr(25)); + assertEquals(25, a.ord("Z")); + assertEquals("?", a.chr(-1)); + assertEquals(StringParser.ERR, a.ord("CH")); + } + + @Test + public void english25DropsQ() { + Locale.setDefault(Locale.ENGLISH); + Alphabet a = Alphabet.getVariantInstance(25, ""); + assertEquals(25, a.count()); + assertEquals("R", a.chr(16)); + assertEquals("Z", a.chr(24)); + assertEquals(StringParser.ERR, a.ord("Q")); + assertEquals(24, a.ord("Z")); + assertEquals(9, a.ord("J")); + } + + @Test + public void english25J() { + Locale.setDefault(Locale.ENGLISH); + Alphabet a = Alphabet.getVariantInstance(25, "J"); + assertEquals(25, a.count()); + assertEquals("I", a.chr(8)); + assertEquals("K", a.chr(9)); + assertEquals(8, a.ord("J")); + assertEquals(8, a.ord("I")); + } + + @Test + public void english25K() { + Locale.setDefault(Locale.ENGLISH); + Alphabet a = Alphabet.getVariantInstance(25, "K"); + assertEquals(25, a.count()); + assertEquals("C", a.chr(2)); + assertEquals("J", a.chr(9)); + assertEquals("L", a.chr(10)); + assertEquals(2, a.ord("K")); + } + + @Test + public void english24DropsQX() { + Locale.setDefault(Locale.ENGLISH); + Alphabet a = Alphabet.getVariantInstance(24, ""); + assertEquals(24, a.count()); + assertEquals("R", a.chr(16)); + assertEquals("Y", a.chr(22)); + assertEquals("Z", a.chr(23)); + assertEquals(StringParser.ERR, a.ord("Q")); + assertEquals(StringParser.ERR, a.ord("X")); + assertEquals(23, a.ord("Z")); + } + + @Test + public void english24JQ() { + Locale.setDefault(Locale.ENGLISH); + Alphabet a = Alphabet.getVariantInstance(24, "JQ"); + assertEquals(24, a.count()); + assertEquals(StringParser.ERR, a.ord("Q")); + assertEquals(8, a.ord("J")); + } + + @Test + public void czech27Full() { + Locale.setDefault(new Locale("cs", "CZ")); + Alphabet a = Alphabet.getVariantInstance(27, ""); + assertEquals(27, a.count()); + assertEquals("CH", a.chr(8)); + assertEquals("I", a.chr(9)); + assertEquals("Z", a.chr(26)); + assertEquals(8, a.ord("CH")); + assertEquals(2, a.ord("C")); + assertEquals(StringParser.ERR, a.ord("CHC")); + } + + @Test + public void czech27ParserChDigraph() { + Locale.setDefault(new Locale("cs", "CZ")); + Alphabet a = Alphabet.getVariantInstance(27, ""); + ArrayList o = ords(a.getStringParser("chj")); + assertEquals(java.util.Arrays.asList(8, 10), o); + ArrayList cc = ords(a.getStringParser("chch")); + assertEquals(java.util.Arrays.asList(8, 8), cc); + } + + @Test + public void czechWithoutCh26() { + Locale.setDefault(new Locale("cs", "CZ")); + Alphabet a = Alphabet.getVariantInstance(26, ""); + assertEquals(26, a.count()); + assertEquals(StringParser.ERR, a.ord("CH")); + assertEquals("I", a.chr(8)); + ArrayList o = ords(a.getStringParser("ch")); + assertEquals(java.util.Arrays.asList(2, 7), o); + } + + @Test + public void czech25DropsQ() { + Locale.setDefault(new Locale("cs", "CZ")); + Alphabet a = Alphabet.getVariantInstance(25, ""); + assertEquals(25, a.count()); + assertEquals(StringParser.ERR, a.ord("Q")); + assertEquals(24, a.ord("Z")); + } + + @Test + public void czech25J() { + Locale.setDefault(new Locale("cs", "CZ")); + Alphabet a = Alphabet.getVariantInstance(25, "J"); + assertEquals(25, a.count()); + assertEquals("I", a.chr(8)); + assertEquals("K", a.chr(9)); + assertEquals(8, a.ord("J")); + } + + @Test + public void czech24DropsQWX() { + Locale.setDefault(new Locale("cs", "CZ")); + Alphabet a = Alphabet.getVariantInstance(24, ""); + assertEquals(24, a.count()); + assertEquals("X", a.chr(21)); + assertEquals("Z", a.chr(23)); + assertEquals(StringParser.ERR, a.ord("Q")); + assertEquals(StringParser.ERR, a.ord("W")); + assertEquals(23, a.ord("Z")); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/common/decoder/StatefulDecoderTest.java b/spa2/src/test/java/cz/absolutno/sifry/common/decoder/StatefulDecoderTest.java new file mode 100644 index 0000000..cedfac7 --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/common/decoder/StatefulDecoderTest.java @@ -0,0 +1,161 @@ +package cz.absolutno.sifry.common.decoder; + +import static org.junit.Assert.assertEquals; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import cz.absolutno.sifry.App; +import cz.absolutno.sifry.R; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 35, qualifiers = "en-rUS") +public class StatefulDecoderTest { + + private SharedPreferences prefs; + + @Before + public void setUp() { + prefs = PreferenceManager.getDefaultSharedPreferences(App.getContext()); + prefs.edit().clear().commit(); + } + + @After + public void tearDown() { + prefs.edit().clear().commit(); + } + + private void setPref(String key, boolean value) { + prefs.edit().putBoolean(key, value).commit(); + } + + private static ArrayList codes(int... x) { + ArrayList l = new ArrayList<>(x.length); + for (int i : x) l.add(i); + return l; + } + + private static String decode(Decoder d, int... x) { + return d.decode(codes(x)); + } + + private static List statesSeen(StatefulDecoder d) { + final List seen = new ArrayList<>(); + d.setOnStateChangedListener(state -> seen.add(state == null ? null : state)); + return seen; + } + + private static List statesOf(String... s) { + return Arrays.asList(s); + } + + // ------------------------------------------------------------------ semafor + + @Test + public void semaforLetters() { + StatefulDecoder d = new StatefulDecoder(R.xml.semafor_decoder); + assertEquals("ABCD", decode(d, 3, 5, 9, 17)); + } + + @Test + public void semaforNumbersViaSwitch() { + StatefulDecoder d = new StatefulDecoder(R.xml.semafor_decoder); + assertEquals("1234", decode(d, 48, 3, 5, 9, 17)); + } + + @Test + public void semaforSwitchToLettersAgain() { + StatefulDecoder d = new StatefulDecoder(R.xml.semafor_decoder); + assertEquals("A1A", decode(d, 3, 48, 3, 80, 3)); + } + + @Test + public void semaforStatesTraced() { + StatefulDecoder d = new StatefulDecoder(R.xml.semafor_decoder); + List seen = statesSeen(d); + decode(d, 3, 48, 3, 80, 3); + assertEquals(statesOf("", "", "Num", "Num", "", ""), seen); + } + + @Test + public void semaforNumbersDisabled() { + setPref("pref_sem_num", false); + StatefulDecoder d = new StatefulDecoder(R.xml.semafor_decoder); + // 48 no longer switches to digits: it is undecodable ("?"), and 3 stays a letter + assertEquals("A?A", decode(d, 3, 48, 3)); + // code 80 is the letter J (flag + digit-on mapping in saSmDPismena), never a + // "back to letters" switch while numbers are disabled + assertEquals("J", decode(d, 80)); + assertEquals("A", decode(d, 3)); + // no Num state ever enters + List seen = statesSeen(d); + decode(d, 3, 48, 3); + assertEquals(statesOf("", "", ""), seen); + } + + // ------------------------------------------------------------------ braille + + @Test + public void brailleLetters() { + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + assertEquals("ABW", decode(d, 1, 3, 58)); + } + + @Test + public void brailleNumbersViaPrefix() { + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + assertEquals("12", decode(d, 60, 1, 3)); + } + + @Test + public void brailleNumberPrefixTogglesInWord() { + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + assertEquals("A1A", decode(d, 1, 60, 1, 48, 1)); + } + + @Test + public void brailleStatesTraced() { + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + List seen = statesSeen(d); + decode(d, 1, 60, 1, 48, 1); + assertEquals(statesOf("", "", "Num", "Num", "Num", "", ""), seen); + } + + @Test + public void brailleLowerCaseFormat() { + setPref("pref_brl_fmt", true); + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + assertEquals("ab", decode(d, 1, 3)); + } + + @Test + public void braillePunctuationViaInterp() { + setPref("pref_brl_itp", true); + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + assertEquals("AB.", decode(d, 1, 3, 50)); + } + + @Test + public void brailleSpaceWhenEnabled() { + setPref("pref_brl_spc", true); + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + assertEquals("A\u00b7B", decode(d, 1, 0, 3)); + } + + @Test + public void brailleSpaceWhenDisabled() { + StatefulDecoder d = new StatefulDecoder(R.xml.braille_decoder); + assertEquals("A?B", decode(d, 1, 0, 3)); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/common/decoder/StringDecoderTest.java b/spa2/src/test/java/cz/absolutno/sifry/common/decoder/StringDecoderTest.java new file mode 100644 index 0000000..7d64eae --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/common/decoder/StringDecoderTest.java @@ -0,0 +1,46 @@ +package cz.absolutno.sifry.common.decoder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import cz.absolutno.sifry.R; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 35, qualifiers = "en-rUS") +public class StringDecoderTest { + + @Test + public void decodesValueAfterColon() { + StringDecoder d = new StringDecoder(R.array.saBRLetters); + assertEquals("A", d.decode(1)); + assertEquals("B", d.decode(3)); + assertEquals("W", d.decode(58)); + assertNull(d.decode(0)); + } + + @Test + public void isAlwaysTemp() { + assertTrue(new StringDecoder(R.array.saBRLetters).isTemp()); + } + + @Test + public void descriptionComesFromMiddleSegment() { + StringDecoder d = new StringDecoder(R.array.saBRLetters); + // entry is "1:A"; desc is the middle segment ("A") + assertEquals("A", d.getDesc(1)); + assertNull(d.getDesc(0)); + } + + @Test + public void invalidInnermostColonStillParses() { + // Braille letters have no inner colon, but a malformed entry still + // rounds trip: make sure a code that does not exist stays null. + assertEquals(null, new StringDecoder(R.array.saBRLetters).decode(12345)); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/frekvence/FrekvExpListAdapterTest.java b/spa2/src/test/java/cz/absolutno/sifry/frekvence/FrekvExpListAdapterTest.java new file mode 100644 index 0000000..346a001 --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/frekvence/FrekvExpListAdapterTest.java @@ -0,0 +1,94 @@ +package cz.absolutno.sifry.frekvence; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Field; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import cz.absolutno.sifry.R; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 35, qualifiers = "en-rUS") +public class FrekvExpListAdapterTest { + + // input "ABAB CD": letters A2 B2 C1 D1, one space, two words + private static final int GROUP_COUNT = 8; + private static final int[] GROUP_IDS = { + R.id.idFDGPismenaC, + R.id.idFDGVseC, + R.id.idFDGPismenaP, + R.id.idFDGDelkyC, + R.id.idFDGDelkyP, + R.id.idFDGPrvni, + R.id.idFDGPosledni, + R.id.idFDGText, + }; + private static final int[] CHILDREN = {3, 2, 26, 1, 4, 1, 1, 5}; + + private static String childS(FrekvExpListAdapter a, int g, int c) { + try { + Object ret = FrekvExpListAdapter.class + .getMethod("getChild", int.class, int.class).invoke(a, g, c); + Field f = ret.getClass().getDeclaredField("s"); + f.setAccessible(true); + return (String) f.get(ret); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + @Test + public void groupStructure() { + FrekvExpListAdapter a = new FrekvExpListAdapter(); + assertEquals(0, a.getGroupCount()); + + a.go("ABAB CD"); + assertEquals(GROUP_COUNT, a.getGroupCount()); + for (int i = 0; i < GROUP_COUNT; i++) + assertEquals(GROUP_IDS[i], a.getGroupId(i)); + for (int i = 0; i < GROUP_COUNT; i++) + assertEquals("children of group " + i, CHILDREN[i], a.getChildrenCount(i)); + } + + @Test + public void mergedLetterRows() { + FrekvExpListAdapter a = new FrekvExpListAdapter(); + a.go("ABAB CD"); + assertEquals("A, B", childS(a, 0, 0)); // count 2 + assertEquals("C, D", childS(a, 0, 1)); // count 1 + } + + @Test + public void mergedAllCharsRows() { + FrekvExpListAdapter a = new FrekvExpListAdapter(); + a.go("ABAB CD"); + assertEquals("A, B", childS(a, 1, 0)); + assertEquals(2, a.getChildrenCount(1)); + } + + @Test + public void wordLengthRows() { + FrekvExpListAdapter a = new FrekvExpListAdapter(); + a.go("ABAB CD"); // words of length 2 and 4 + assertEquals("2, 4", childS(a, 3, 0)); // both lengths, merged (equal count) + assertEquals(1, a.getChildrenCount(3)); + } + + @Test + public void repeatedGoResets() { + FrekvExpListAdapter a = new FrekvExpListAdapter(); + a.go("ABAB CD"); + a.go("XY"); // single word: no delky/prvni/posledni/... groups + assertEquals(3, a.getGroupCount()); + assertEquals(R.id.idFDGPismenaC, a.getGroupId(0)); + assertEquals(R.id.idFDGPismenaP, a.getGroupId(1)); + assertEquals(R.id.idFDGText, a.getGroupId(2)); + assertEquals(2, a.getChildrenCount(0)); // "X, Y" merged (count 1) + zero-count row + assertEquals(26, a.getChildrenCount(1)); + assertEquals(4, a.getChildrenCount(2)); // letters, digits, spaces, all + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/morse/MorseDecoderTest.java b/spa2/src/test/java/cz/absolutno/sifry/morse/MorseDecoderTest.java new file mode 100644 index 0000000..7ee99f3 --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/morse/MorseDecoderTest.java @@ -0,0 +1,89 @@ +package cz.absolutno.sifry.morse; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 35, qualifiers = "en-rUS") +public class MorseDecoderTest { + + private static ArrayList codes(int... x) { + ArrayList l = new ArrayList<>(x.length); + for (int i : x) l.add(i); + return l; + } + + @Test + public void letters() { + MorseDecoder d = new MorseDecoder(); + assertEquals("A", d.decode(5)); + assertEquals("B", d.decode(24)); + assertEquals("E", d.decode(2)); + assertEquals("Z", d.decode(28)); + } + + @Test + public void digits() { + MorseDecoder d = new MorseDecoder(); + assertEquals("0", d.decode(63)); + assertEquals("1", d.decode(47)); + assertEquals("2", d.decode(39)); + assertEquals("3", d.decode(35)); + assertEquals("4", d.decode(33)); + assertEquals("5", d.decode(32)); + } + + @Test + public void punctuation() { + MorseDecoder d = new MorseDecoder(); + assertEquals(".", d.decode(85)); + assertEquals(",", d.decode(115)); + assertEquals("?", d.decode(76)); + assertEquals("'", d.decode(94)); + assertEquals("\"", d.decode(82)); + assertEquals("/", d.decode(50)); + assertEquals("(", d.decode(54)); + assertEquals(")", d.decode(109)); + assertEquals("&", d.decode(40)); + assertEquals(":", d.decode(120)); + assertEquals(";", d.decode(106)); + assertEquals("=", d.decode(49)); + assertEquals("+", d.decode(42)); + assertEquals("\u2013", d.decode(97)); + assertEquals("_", d.decode(77)); + assertEquals("$", d.decode(137)); + assertEquals("@", d.decode(90)); + } + + @Test + public void addedSeparatorAndUnknown() { + MorseDecoder d = new MorseDecoder(); + assertEquals("\u00b7", d.decode(1)); + assertEquals("?", d.decode(0)); + assertEquals("?", d.decode(9999)); + } + + @Test + public void tuplesAndEncode() { + MorseDecoder d = new MorseDecoder(); + assertEquals("AB", d.decode(codes(5, 24))); + ArrayList out = new ArrayList<>(); + assertEquals(false, d.encode("AB", out)); + assertEquals(codes(5, 24), out); + assertEquals(false, d.encode("01", out)); + assertEquals(codes(63, 47), out); + } + + @Test + public void digitsTuple() { + MorseDecoder d = new MorseDecoder(); + assertEquals("012", d.decode(codes(63, 47, 39))); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/substituce/SubstituceAdaptersTest.java b/spa2/src/test/java/cz/absolutno/sifry/substituce/SubstituceAdaptersTest.java new file mode 100644 index 0000000..f22c4e5 --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/substituce/SubstituceAdaptersTest.java @@ -0,0 +1,100 @@ +package cz.absolutno.sifry.substituce; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import cz.absolutno.sifry.common.alphabet.PlainEnglishAlphabet; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 35, qualifiers = "en-rUS") +public class SubstituceAdaptersTest { + + private static PlainEnglishAlphabet en() { + return new PlainEnglishAlphabet(); + } + + @Test + public void posuny() { + PosunyAdapter p = new PosunyAdapter(en()); + assertEquals(0, p.getCount()); + p.setInput("AB"); + assertEquals(26, p.getCount()); + assertEquals("AB", p.getItem(0)); + assertEquals("BC", p.getItem(1)); + assertEquals("ZA", p.getItem(25)); + p.clear(); + assertEquals(0, p.getCount()); + } + + @Test + public void atbash() { + AtbashAdapter a = new AtbashAdapter(en()); + a.setInput("AB"); + assertEquals("ZY", a.getItem(0)); + } + + @Test + public void afinni() { + AfinniAdapter f = new AfinniAdapter(en()); + f.setInput("AB"); + assertEquals("BC", f.getItem(1)); + f.setCoeff(3); + assertEquals("AD", f.getItem(0)); + } + + @Test + public void heslo() { + HesloAdapter h = new HesloAdapter(en()); + assertEquals(0, h.getCount()); + h.setKey("B"); + h.setInput("A"); + assertEquals(h.getCountValid(), h.getCount()); + assertEquals("C", h.getItem(0)); + assertEquals("Y", h.getItem(1)); + assertEquals("A", h.getItem(2)); + assertEquals("B", h.getItem(3)); + assertEquals("Z", h.getItem(4)); + assertEquals("B", h.getItem(5)); + } + + @Test + public void autokey() { + AutokeyAdapter k = new AutokeyAdapter(en()); + k.setInput("AB"); + assertEquals("AC", k.getItem(0)); + assertEquals("AA", k.getItem(1)); + assertEquals("AC", k.getItem(12)); + } + + @Test + public void pozice() { + PoziceAdapter p = new PoziceAdapter(en()); + p.setInput("AB"); + assertEquals("BD", p.getItem(0)); + assertEquals("ZZ", p.getItem(1)); + assertEquals("AC", p.getItem(3)); + assertEquals("AA", p.getItem(4)); + assertEquals("AA", p.getItem(5)); + assertEquals("AB", p.getItem(6)); + assertEquals("AB", p.getItem(7)); + } + + @Test + public void klic() { + KlicAdapter k = new KlicAdapter(en()); + assertEquals(0, k.getCount()); + k.setKlic("SIF", 0); + k.setInput("SIF"); + assertEquals(2, k.getCount()); + assertEquals("ABC", k.getItem(0)); + k.clear(); + assertEquals(0, k.getCount()); + k.setKlic("SIF", 0); + k.setInput("ABC"); + assertEquals("SIF", k.getItem(1)); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/tabulky/mobil/MobilDecoderTest.java b/spa2/src/test/java/cz/absolutno/sifry/tabulky/mobil/MobilDecoderTest.java new file mode 100644 index 0000000..6783c9a --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/tabulky/mobil/MobilDecoderTest.java @@ -0,0 +1,70 @@ +package cz.absolutno.sifry.tabulky.mobil; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +public class MobilDecoderTest { + + private static ArrayList codes(int... x) { + ArrayList l = new ArrayList<>(x.length); + for (int i : x) l.add(i); + return l; + } + + @Test + public void decodesTuple() { + MobilDecoder d = new MobilDecoder(); + assertEquals("WORD", d.decode(codes(0x80, 0x52, 0x62, 0x20))); + assertEquals("W", d.decode(0x80)); + assertEquals("O", d.decode(0x52)); + assertEquals("R", d.decode(0x62)); + assertEquals("D", d.decode(0x20)); + } + + @Test + public void decodesSingleKeyFirstLetter() { + assertEquals("J", new MobilDecoder().decode(0x40)); + } + + @Test + public void invalidKeyOrRepReturnsNull() { + MobilDecoder d = new MobilDecoder(); + assertNull(d.decode(0)); // key 0 has no letters + assertNull(d.decode(0x90)); // key 9 has no letters + assertNull(d.decode(0x53)); // key 5 'MNO' is length 3, rep 3 invalid + } + + @Test + public void encodesWord() { + MobilDecoder d = new MobilDecoder(); + ArrayList out = new ArrayList<>(); + assertFalse(d.encode("WORD", out)); + assertEquals(codes(0x80, 0x52, 0x62, 0x20), out); + } + + @Test + public void encodesLowercaseSameAsUppercase() { + MobilDecoder d = new MobilDecoder(); + ArrayList a = new ArrayList<>(); + ArrayList b = new ArrayList<>(); + d.encode("word", a); + d.encode("WORD", b); + assertEquals(b, a); + } + + @Test + public void letterAccessors() { + MobilDecoder d = new MobilDecoder(); + assertEquals(3, d.getNumLetters(1)); + assertEquals("S", d.getLetter(6, 3)); + assertEquals("W", d.getLetter(8, 0)); + assertArrayEquals(new String[]{"P", "Q", "R", "S"}, d.getLetters(6)); + } +} \ No newline at end of file diff --git a/spa2/src/test/java/cz/absolutno/sifry/tabulky/polsky/PolskyKrizDecoderTest.java b/spa2/src/test/java/cz/absolutno/sifry/tabulky/polsky/PolskyKrizDecoderTest.java new file mode 100644 index 0000000..ef741f5 --- /dev/null +++ b/spa2/src/test/java/cz/absolutno/sifry/tabulky/polsky/PolskyKrizDecoderTest.java @@ -0,0 +1,68 @@ +package cz.absolutno.sifry.tabulky.polsky; + +import static org.junit.Assert.assertEquals; + +import java.util.Locale; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class PolskyKrizDecoderTest { + + private Locale saved; + + @Before + public void saveLocale() { + saved = Locale.getDefault(); + } + + @After + public void restoreLocale() { + Locale.setDefault(saved); + } + + private static int[] kriz(int a, int b, int c) { + return new int[]{a, b, c}; + } + + @Test + public void czechLetters() { + Locale.setDefault(new Locale("cs", "CZ")); + PolskyKrizDecoder d = new PolskyKrizDecoder(); + assertEquals("A", d.decode(kriz(0, 0, 0))); + assertEquals("H", d.decode(kriz(0, 2, 1))); + assertEquals("CH", d.decode(kriz(0, 2, 2))); + assertEquals("Z", d.decode(kriz(2, 2, 2))); + } + + @Test + public void czechWithoutCh() { + Locale.setDefault(new Locale("cs", "CZ")); + PolskyKrizDecoder d = new PolskyKrizDecoder("-Ch"); + assertEquals("A", d.decode(kriz(0, 0, 0))); + assertEquals("I", d.decode(kriz(0, 2, 2))); + assertEquals("?", d.decode(kriz(2, 2, 2))); + } + + @Test + public void englishVariantIsPlain26() { + Locale.setDefault(Locale.ENGLISH); + PolskyKrizDecoder d = new PolskyKrizDecoder(); + assertEquals("A", d.decode(kriz(0, 0, 0))); + assertEquals("I", d.decode(kriz(0, 2, 2))); + assertEquals("?", d.decode(kriz(2, 2, 2))); + } + + @Test + public void setVarSwitchesInstance() { + Locale.setDefault(new Locale("cs", "CZ")); + PolskyKrizDecoder d = new PolskyKrizDecoder(); + d.setVar("-Ch"); + assertEquals("I", d.decode(kriz(0, 2, 2))); + assertEquals("?", d.decode(kriz(2, 2, 2))); + d.setVar(""); + assertEquals("CH", d.decode(kriz(0, 2, 2))); + assertEquals("Z", d.decode(kriz(2, 2, 2))); + } +} \ No newline at end of file