Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.github.maxdac.marknotes.core.parity

import java.io.File
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder

/** The `appliesTo` selection of [FixtureSet], on synthetic fixture files (no shared spec checkout needed). */
class FixtureSetTest {
@get:Rule
val tmp = TemporaryFolder()

private fun fixtures(): File = tmp.newFolder("fixtures").also { dir ->
write(dir, "front-matter/split", null)
write(dir, "issues/validate", """["sync", "desktop", "android-pro"]""")
write(dir, "issues/pull", """["sync", "desktop", "android-pro"]""")
write(dir, "everywhere/some", """["sync", "android", "android-pro"]""")
}

private fun write(dir: File, id: String, appliesTo: String?) {
val file = File(dir, "$id.json").also { it.parentFile.mkdirs() }
val applies = appliesTo?.let { """"appliesTo": $it,""" }.orEmpty()
file.writeText(
"""{ $applies "cases": [ { "name": "c", "input": {"a": "b"}, "expected": {"x": 1} } ] }"""
)
}

@Test
fun libreSkipsIssuesFiles() {
val set = FixtureSet(fixtures())
assertEquals(listOf("everywhere/some", "front-matter/split"), set.ids())
assertEquals(listOf("issues/pull", "issues/validate"), set.skippedIds())
assertEquals(emptyList<FixtureCase>(), set.load("issues/pull"))
}

@Test
fun proSelectsIssuesFiles() {
val set = FixtureSet(fixtures(), SpecFixtures.PRO_KEY)
assertEquals(
listOf("everywhere/some", "front-matter/split", "issues/pull", "issues/validate"),
set.ids()
)
assertEquals(emptyList<String>(), set.skippedIds())
assertEquals(1, set.load("issues/pull").size)
}

@Test
fun filesWithoutAppliesToApplyToEveryone() {
val dir = fixtures()
assertTrue(FixtureSet(dir).isApplicable("front-matter/split"))
assertTrue(FixtureSet(dir, "web").isApplicable("front-matter/split"))
}

@Test
fun proAlsoRunsFilesListingOnlyAndroid() {
val dir = fixtures()
write(dir, "libre/only", """["android"]""")
assertTrue(FixtureSet(dir, SpecFixtures.PRO_KEY).isApplicable("libre/only"))
assertTrue(FixtureSet(dir).isApplicable("libre/only"))
assertEquals(false, FixtureSet(dir, "web").isApplicable("libre/only"))
}

@Test
fun anUnmappedFileStillFails() {
// The coverage rule of SpecCoverageTest: applicable ids must equal the mapped ones.
val set = FixtureSet(fixtures())
val mapped = listOf("front-matter/split")
assertEquals(listOf("everywhere/some"), set.ids() - mapped.toSet())
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.maxdac.marknotes.core.parity

import org.junit.Assert.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
Expand Down Expand Up @@ -35,6 +36,15 @@ class SpecCoverageTest {
)
}

@Test
fun filesExcludedByAppliesToAreSkipped() {
val skipped = SpecFixtures.skippedIds()
assumeTrue("every fixture file applies to Libre", skipped.isNotEmpty())
// Recognised, not run and not unmapped.
assertEquals(emptyList<String>(), skipped.filter(CoreParity.mappings::containsKey))
assumeTrue("Skipped by appliesTo for ${SpecFixtures.APP_KEY}: $skipped", false)
}

@Test
fun pendingSpecIssuesReferToExistingCases() {
SpecFixtures.requireDir()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,69 @@ class FixtureCase(
}
}

/** Loads the shared golden fixtures from `marknotes.specDir` (Gradle property, default `../spec`). The
* fixtures are not part of this repository: without them the parity tests skip. */
/**
* The fixture files under one `fixtures/` folder, as seen by the runner of [edition]: `android` for Libre,
* `android-pro` for Pro. A file with an `appliesTo` list that lacks [edition] is recognised but not applicable:
* it is reported as skipped, never run and never counted as unmapped. Files without `appliesTo` apply to all.
*/
class FixtureSet(private val dir: File, val edition: String = SpecFixtures.APP_KEY) {
private val json = Json { ignoreUnknownKeys = true }

/** Ids ("area/function") of every fixture file, sorted, applicable or not. */
fun allIds(): List<String> = dir.walkTopDown()
.filter { it.isFile && it.extension == "json" }
.map { it.relativeTo(dir).invariantSeparatorsPath.removeSuffix(".json") }
.sorted()
.toList()

/** The runner keys listed by the file's `appliesTo`, or null when it applies to every runner. */
fun appliesTo(
id: String
): Set<String>? = root(id)["appliesTo"]?.jsonArray?.map { it.jsonPrimitive.content }?.toSet()

/** `android` covers Libre and Pro, so Pro (`android-pro`) also runs files that list only `android`. */
fun isApplicable(id: String): Boolean {
val keys = appliesTo(id) ?: return true
return edition in keys || (edition == SpecFixtures.PRO_KEY && SpecFixtures.APP_KEY in keys)
}

/** Ids of the files this edition must run (or map as "elsewhere"). */
fun ids(): List<String> = allIds().filter(::isApplicable)

/** Ids of the files recognised but excluded for this edition by `appliesTo`. */
fun skippedIds(): List<String> = allIds().filterNot(::isApplicable)

/** Cases of fixture [id]; none when the file does not apply to this edition. */
fun load(id: String): List<FixtureCase> {
if (!isApplicable(id)) return emptyList()
return root(id).getValue("cases").jsonArray.map { element ->
val case = element.jsonObject
FixtureCase(
fixture = id,
name = case.getValue("name").jsonPrimitive.content,
input = case.getValue("input").jsonObject,
expected = case.getValue("expected"),
// Divergences are keyed by app (`android`), not by edition: Pro shares Libre's.
knownDivergence = (case["knownDivergence"] as? JsonObject)
?.get(SpecFixtures.APP_KEY)?.jsonPrimitive?.content
)
}
}

private fun root(id: String): JsonObject = json.parseToJsonElement(File(dir, "$id.json").readText()).jsonObject
}

/**
* Loads the shared golden fixtures from `marknotes.specDir` (Gradle property, default `../spec`). The fixtures
* are not part of this repository: without them the parity tests skip. Every function takes the runner's
* [edition] key (default [APP_KEY]); Pro passes [PRO_KEY] to also run the `android-pro` files.
*/
object SpecFixtures {
/** Runner key of Libre, and of Pro for files that apply to every Android edition. */
const val APP_KEY = "android"

private val json = Json { ignoreUnknownKeys = true }
/** Runner key of Pro (MarkNotes Plus), selected by `appliesTo: ["android-pro"]`. */
const val PRO_KEY = "android-pro"

private val fixturesDir: File?
get() = System.getProperty("marknotes.specDir")?.let { File(it, "fixtures") }?.takeIf { it.isDirectory }
Expand All @@ -62,6 +119,9 @@ object SpecFixtures {
/** True when `marknotes.parity.required` is set: missing fixtures are then an error instead of a skip. */
private val required: Boolean get() = System.getProperty("marknotes.parity.required").toBoolean()

/** The fixtures of [edition]; skips the calling test when the fixtures aren't there (fails if [required]). */
fun requireSet(edition: String = APP_KEY): FixtureSet = FixtureSet(requireDir(), edition)

/** The fixtures folder; skips the calling test when the fixtures aren't there (fails if [required]). */
fun requireDir(): File {
val dir = fixturesDir
Expand All @@ -70,31 +130,18 @@ object SpecFixtures {
return dir!!
}

/** Ids ("area/function") of every fixture file, sorted. */
fun ids(): List<String> {
val dir = requireDir()
return dir.walkTopDown()
.filter { it.isFile && it.extension == "json" }
.map { it.relativeTo(dir).invariantSeparatorsPath.removeSuffix(".json") }
.sorted()
.toList()
}
/** Ids ("area/function") of the fixture files that apply to [edition], sorted. */
fun ids(edition: String = APP_KEY): List<String> = requireSet(edition).ids()

/** Cases of fixture [id], or none when the fixtures aren't there (an error when [required]). */
fun load(id: String): List<FixtureCase> {
/** Ids of the files [edition] recognises but skips through `appliesTo`. */
fun skippedIds(edition: String = APP_KEY): List<String> = requireSet(edition).skippedIds()

/** Cases of fixture [id], or none when the fixtures aren't there (an error when [required]) or the file does
* not apply to [edition]. */
fun load(id: String, edition: String = APP_KEY): List<FixtureCase> {
val dir = fixturesDir
check(dir != null || !required) { MISSING_FIXTURES }
if (dir == null) return emptyList()
val root = json.parseToJsonElement(File(dir, "$id.json").readText()).jsonObject
return root.getValue("cases").jsonArray.map { element ->
val case = element.jsonObject
FixtureCase(
fixture = id,
name = case.getValue("name").jsonPrimitive.content,
input = case.getValue("input").jsonObject,
expected = case.getValue("expected"),
knownDivergence = (case["knownDivergence"] as? JsonObject)?.get(APP_KEY)?.jsonPrimitive?.content
)
}
return FixtureSet(dir, edition).load(id)
}
}
Loading