mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 07:04:35 +00:00
Build all apps from Markdown content; Compose Desktop replaces GTK on Linux and Windows
- content/ + scripts/build-content.py produce content.json for every app - stable lesson IDs with one-time progress migration (Swift, Kotlin) - shared answer-check tests (tests/answers.json) for Kotlin and Swift - android/desktop: Compose app with updater, speech and GTK progress import - CI packages AppImage and Windows zip with ProGuard, runs the tests Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b324496334
commit
c3bc32113b
964 changed files with 57285 additions and 1156 deletions
104
tests/answers.json
Normal file
104
tests/answers.json
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"_about": "Answer checking cases shared by the Swift (iOS, macOS) and Kotlin (Android, desktop) apps. Both must agree with every line. Run with `make test`.",
|
||||
"normalize": [
|
||||
["Größe", "groesse"],
|
||||
["Über", "ueber"],
|
||||
[" Hallo, Welt! ", "hallo welt"],
|
||||
["Příliš žluťoučký kůň", "prilis zlutoucky kun"],
|
||||
["a\tb\nc", "a b c"],
|
||||
["e-mail", "email"],
|
||||
["a , b", "a b"],
|
||||
["", ""]
|
||||
],
|
||||
"accepts": [
|
||||
["Deutschland", "deutschland|eutschland", true],
|
||||
["Osterreich", "österreich|osterreich", true],
|
||||
["Oesterreich", "österreich", true],
|
||||
["hasnt", "hasn't|has not", true],
|
||||
["has not", "hasn't|has not", true],
|
||||
["Seděla", "seděla|sedla", true],
|
||||
["sedela", "seděla|sedla", true],
|
||||
["is", "are", false],
|
||||
["", "", false],
|
||||
["x", null, false]
|
||||
],
|
||||
"incomplete": [
|
||||
["der", "der Vater", true],
|
||||
["der vater", "der Vater", false],
|
||||
["", "der Vater", false],
|
||||
["die", "der Vater|die Mutter", true]
|
||||
],
|
||||
"netTxtEq": [
|
||||
["10.0.0.0", " 10.0.0.0 ", true],
|
||||
["ABC", "abc", true],
|
||||
["10.0.0.1", "10.0.0.2", false],
|
||||
[null, null, true],
|
||||
["a", null, false]
|
||||
],
|
||||
"hasUmlaut": [
|
||||
["Mädchen", true],
|
||||
["Straße", true],
|
||||
["Strasse", false],
|
||||
[null, false]
|
||||
],
|
||||
"math": [
|
||||
["num:12", "12", true],
|
||||
["num:12", "12.0", true],
|
||||
["num:12", "12,0", true],
|
||||
["num:12", "12 cm", true],
|
||||
["num:12", "13", false],
|
||||
["num:12", "", false],
|
||||
["num:12", "abc", false],
|
||||
["num:1/2", "0,5", true],
|
||||
["num:1/2", "2/4", true],
|
||||
["num:1/2", "1/3", false],
|
||||
["num:3/2", "1 1/2", true],
|
||||
["num:-3", "−3", true],
|
||||
["num:50", "50 %", true],
|
||||
["num:2√3", "√12", true],
|
||||
["num:2√3", "sqrt(12)", true],
|
||||
["num:2√3", "3.4641", true],
|
||||
["num:2√3", "3.46", false],
|
||||
["num:2π", "2pi", true],
|
||||
["num:2π", "6.283", false],
|
||||
["set:2;3", "3; 2", true],
|
||||
["set:2;3", "2, 3", true],
|
||||
["set:2;3", "2;3;4", false],
|
||||
["set:2;3", "2", false],
|
||||
["pair:2;-1", "2; -1", true],
|
||||
["pair:2;-1", "-1; 2", false],
|
||||
["word:ano|yes", "Ano", true],
|
||||
["word:ano|yes", "áno", true],
|
||||
["word:ano|yes", "ne", false],
|
||||
[null, "1", false]
|
||||
],
|
||||
"drawNeed": [
|
||||
["square:2,3;5,3", 2],
|
||||
["mid:0,0;4,4", 1],
|
||||
["points:1,4;fix:1,1;fix:5,1", 1],
|
||||
["num:3", 0]
|
||||
],
|
||||
"drawGiven": [
|
||||
["points:1,4;fix:1,1;fix:5,1", [[1, 1], [5, 1]]],
|
||||
["mid:0,0;4,4", [[0, 0], [4, 4]]],
|
||||
["para:0,0;2,0;0,2", [[0, 0], [2, 0], [0, 2]]],
|
||||
["num:3", []]
|
||||
],
|
||||
"draw": [
|
||||
["mid:0,0;4,4", [[2, 2]], true],
|
||||
["mid:0,0;4,4", [[2, 3]], false],
|
||||
["mid:0,0;3,4", [[1, 2]], false],
|
||||
["para:0,0;2,0;0,2", [[2, 2]], true],
|
||||
["para:0,0;2,0;0,2", [[2, 1]], false],
|
||||
["right:2,4;8,4", [[5, 1]], true],
|
||||
["right:2,4;8,4", [[5, 4]], false],
|
||||
["square:2,3;5,3", [[5, 6], [2, 6]], true],
|
||||
["square:2,3;5,3", [[2, 6], [5, 6]], true],
|
||||
["square:2,3;5,3", [[5, 0], [2, 0]], true],
|
||||
["square:2,3;5,3", [[5, 5], [2, 5]], false],
|
||||
["points:1,4;fix:1,1", [[1, 4]], true],
|
||||
["points:1,4;fix:1,1", [[1, 5]], false],
|
||||
["points:1,4;fix:1,1", [[11, 0]], false],
|
||||
["num:3", [], false]
|
||||
]
|
||||
}
|
||||
64
tests/swift/main.swift
Normal file
64
tests/swift/main.swift
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Runs tests/answers.json against ios/Graduately/Data/Answers.swift, the same
|
||||
// cases the Kotlin apps check (android/desktop/src/test). Usage:
|
||||
// swift-answers <tests/answers.json> <content.json>
|
||||
import Foundation
|
||||
|
||||
let args = CommandLine.arguments
|
||||
guard args.count == 3,
|
||||
let casesData = FileManager.default.contents(atPath: args[1]),
|
||||
let cases = try? JSONSerialization.jsonObject(with: casesData) as? [String: Any],
|
||||
let contentData = FileManager.default.contents(atPath: args[2]),
|
||||
let content = try? JSONSerialization.jsonObject(with: contentData) as? [String: Any]
|
||||
else {
|
||||
print("usage: swift-answers tests/answers.json content.json")
|
||||
exit(2)
|
||||
}
|
||||
|
||||
var failures = 0
|
||||
var checked = 0
|
||||
|
||||
func rows(_ name: String) -> [[Any]] { cases[name] as? [[Any]] ?? [] }
|
||||
func str(_ v: Any) -> String? { v as? String }
|
||||
func points(_ v: Any) -> [(Int, Int)] { (v as? [[Int]] ?? []).map { ($0[0], $0[1]) } }
|
||||
|
||||
func expect<T: Equatable>(_ got: T, _ want: T, _ what: String) {
|
||||
checked += 1
|
||||
if got != want {
|
||||
failures += 1
|
||||
print("FAIL \(what): got \(got), want \(want)")
|
||||
}
|
||||
}
|
||||
|
||||
for r in rows("normalize") { expect(normalizeAnswer(str(r[0])), str(r[1]) ?? "", "normalize \(r[0])") }
|
||||
for r in rows("accepts") { expect(answerAccepts(normalizeAnswer(str(r[0])), str(r[1])), r[2] as! Bool, "accepts \(r)") }
|
||||
for r in rows("incomplete") { expect(answerIncomplete(normalizeAnswer(str(r[0])), str(r[1])), r[2] as! Bool, "incomplete \(r)") }
|
||||
for r in rows("netTxtEq") { expect(netTxtEq(str(r[0]), str(r[1])), r[2] as! Bool, "netTxtEq \(r)") }
|
||||
for r in rows("hasUmlaut") { expect(hasUmlaut(str(r[0])), r[1] as! Bool, "hasUmlaut \(r)") }
|
||||
for r in rows("math") { expect(mathAnswerOk(str(r[0]), str(r[1])), r[2] as! Bool, "math \(r)") }
|
||||
for r in rows("drawNeed") { expect(mathDrawNeed(str(r[0])), r[1] as! Int, "drawNeed \(r)") }
|
||||
for r in rows("drawGiven") {
|
||||
expect(mathDrawGiven(str(r[0])).map { "\($0.0),\($0.1)" }, points(r[1]).map { "\($0.0),\($0.1)" }, "drawGiven \(r)")
|
||||
}
|
||||
for r in rows("draw") { expect(mathDrawOk(str(r[0]), points(r[1])), r[2] as! Bool, "draw \(r)") }
|
||||
|
||||
// Every worked answer in content/ must pass its own check.
|
||||
var problems = 0
|
||||
for section in ["mat0", "mat", "mat2", "mat3", "mat4"] {
|
||||
for lesson in content[section] as? [[String: Any]] ?? [] {
|
||||
for p in lesson["problems"] as? [[String: Any]] ?? [] {
|
||||
guard let spec = p["answer"] as? String else { continue }
|
||||
let at = "\(section) \(lesson["key"] ?? ""): \(spec)"
|
||||
if mathIsDraw(spec) {
|
||||
expect(mathDrawNeed(spec) > 0, true, at)
|
||||
} else {
|
||||
let body = spec.split(separator: ":", maxSplits: 1).last.map(String.init) ?? spec
|
||||
expect(mathAnswerOk(spec, body.components(separatedBy: "|")[0]), true, at)
|
||||
}
|
||||
problems += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(problems > 100, true, "content has math problems")
|
||||
|
||||
print("swift: \(checked - failures)/\(checked) ok")
|
||||
exit(failures == 0 ? 0 : 1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue