Files
brov-macbook/tests/GitHubPulseTests.swift
T
2026-10-06 19:34:32 +03:00

356 lines
18 KiB
Swift

import Foundation
@main
enum GitHubPulseTests {
static var failures = 0
static func check(_ label: String, _ got: Bool) {
if got { print(" ✓ \(label)") }
else { print(" ✗ \(label)"); failures += 1 }
}
// MARK: - Fixture JSON
// Valid response: PR #42 SUCCESS/APPROVED, PR #43 null CI (unknown)/null review,
// repo "testuser/myrepo" PENDING, archived repo filtered out,
// one to-review PR from another repo.
static let validJSON: Data = """
{
"data": {
"viewer": {
"login": "testuser",
"pullRequests": {
"nodes": [
{
"number": 42,
"title": "Add feature",
"url": "https://github.com/testuser/myrepo/pull/42",
"isDraft": false,
"reviewDecision": "APPROVED",
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
"commits": {"nodes": [{"commit": {"statusCheckRollup": {"state": "SUCCESS"}}}]}
},
{
"number": 43,
"title": "Fix bug",
"url": "https://github.com/testuser/myrepo/pull/43",
"isDraft": true,
"reviewDecision": null,
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
"commits": {"nodes": [{"commit": {"statusCheckRollup": null}}]}
}
]
},
"repositories": {
"nodes": [
{
"nameWithOwner": "testuser/myrepo",
"url": "https://github.com/testuser/myrepo",
"isArchived": false,
"defaultBranchRef": {
"name": "main",
"target": {"statusCheckRollup": {"state": "PENDING"}}
}
},
{
"nameWithOwner": "testuser/archived",
"url": "https://github.com/testuser/archived",
"isArchived": true,
"defaultBranchRef": {
"name": "main",
"target": {"statusCheckRollup": {"state": "SUCCESS"}}
}
}
]
}
},
"reviewRequested": {
"issueCount": 1,
"nodes": [
{
"number": 7,
"title": "Review this",
"url": "https://github.com/other/repo/pull/7",
"isDraft": false,
"author": {"login": "otheruser"},
"repository": {"nameWithOwner": "other/repo", "url": "https://github.com/other/repo"}
}
]
}
}
}
""".data(using: .utf8)!
static let emptyJSON: Data = """
{
"data": {
"viewer": {
"login": "testuser",
"pullRequests": {"nodes": []},
"repositories": {"nodes": []}
},
"reviewRequested": {"issueCount": 0, "nodes": []}
}
}
""".data(using: .utf8)!
// MARK: - Main
static func main() {
// ── GitHubPulse.parse ─────────────────────────────────────────────────
print("GitHubPulse.parse")
do {
let pulse = GitHubPulse.parse(validJSON)
check("parse returns non-nil", pulse != nil)
check("login", pulse?.login == "testuser")
check("2 myPRs", pulse?.myPRs.count == 2)
check("PR#42 id", pulse?.myPRs[0].id == "testuser/myrepo#42")
check("PR#42 ci success", pulse?.myPRs[0].ci == .success)
check("PR#42 review approved", pulse?.myPRs[0].review == .approved)
check("PR#43 ci unknown (null rollup)",pulse?.myPRs[1].ci == .unknown)
check("PR#43 review unknown (null)", pulse?.myPRs[1].review == .unknown)
check("PR#43 isDraft", pulse?.myPRs[1].isDraft == true)
check("1 mainCI (archived filtered)", pulse?.mainCI.count == 1)
check("mainCI repo", pulse?.mainCI[0].repo == "testuser/myrepo")
check("mainCI ci pending", pulse?.mainCI[0].ci == .pending)
check("mainCI branch main", pulse?.mainCI[0].branch == "main")
check("1 toReview", pulse?.toReview.count == 1)
check("toReview id", pulse?.toReview[0].id == "other/repo#7")
check("hasPending (mainCI pending)", pulse?.hasPending == true)
}
// ── GitHubPulse.parse — empty ─────────────────────────────────────────
print("GitHubPulse.parse — empty")
do {
let pulse = GitHubPulse.parse(emptyJSON)
check("empty parses ok", pulse != nil)
check("no myPRs", pulse?.myPRs.isEmpty == true)
check("no mainCI", pulse?.mainCI.isEmpty == true)
check("no toReview", pulse?.toReview.isEmpty == true)
check("!hasPending", pulse?.hasPending == false)
}
// ── GitHubPulse.parse — bad data ──────────────────────────────────────
print("GitHubPulse.parse — bad data")
do {
check("garbage → nil", GitHubPulse.parse("garbage".data(using: .utf8)!) == nil)
check("empty obj → nil", GitHubPulse.parse("{}".data(using: .utf8)!) == nil)
}
// ── CIState init ──────────────────────────────────────────────────────
print("CIState init")
do {
check("nil → unknown", CIState(rawGitHub: nil) == .unknown)
check("PENDING → pending", CIState(rawGitHub: "PENDING") == .pending)
check("EXPECTED → pending", CIState(rawGitHub: "EXPECTED") == .pending)
check("SUCCESS → success", CIState(rawGitHub: "SUCCESS") == .success)
check("FAILURE → failure", CIState(rawGitHub: "FAILURE") == .failure)
check("ERROR → failure", CIState(rawGitHub: "ERROR") == .failure)
check("other → unknown", CIState(rawGitHub: "WAITING") == .unknown)
}
// ── GitHubPulse.events — first poll silent ────────────────────────────
print("GitHubPulse.events — first poll silent")
do {
let pulse = GitHubPulse.parse(validJSON)!
let events = GitHubPulse.events(old: nil, new: pulse)
check("first poll → no events", events.isEmpty)
}
// ── GitHubPulse.events — pending → success ────────────────────────────
print("GitHubPulse.events — pending → success")
do {
var oldPulse = GitHubPulse.parse(emptyJSON)!
oldPulse.myPRs = [GitHubPR(id: "r/p#1", title: "T", url: "", repo: "r/p",
number: 1, isDraft: false, ci: .pending, review: .unknown)]
var newPulse = oldPulse
newPulse.myPRs[0].ci = .success
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("pending→success → ciPassed", events == [.ciPassed(prId: "r/p#1")])
}
// ── GitHubPulse.events — success → failure ────────────────────────────
print("GitHubPulse.events — success → failure")
do {
var oldPulse = GitHubPulse.parse(emptyJSON)!
oldPulse.myPRs = [GitHubPR(id: "r/p#2", title: "T", url: "", repo: "r/p",
number: 2, isDraft: false, ci: .success, review: .unknown)]
var newPulse = oldPulse
newPulse.myPRs[0].ci = .failure
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("success→failure → ciFailed", events == [.ciFailed(prId: "r/p#2")])
}
// ── GitHubPulse.events — main CI failure ──────────────────────────────
print("GitHubPulse.events — main CI failure")
do {
var oldPulse = GitHubPulse.parse(emptyJSON)!
oldPulse.mainCI = [GitHubRepoCI(repo: "a/b", url: "", branch: "main", ci: .success)]
var newPulse = oldPulse
newPulse.mainCI[0].ci = .failure
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("main success→failure → mainFailed", events == [.mainFailed(repo: "a/b")])
}
// ── GitHubPulse.events — new review request ───────────────────────────
print("GitHubPulse.events — new review request")
do {
let pr = GitHubPR(id: "o/r#7", title: "R", url: "", repo: "o/r",
number: 7, isDraft: false, ci: .unknown, review: .pending)
var oldPulse = GitHubPulse.parse(emptyJSON)!
var newPulse = oldPulse
newPulse.toReview = [pr]
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("new review → reviewRequested", events == [.reviewRequested(prId: "o/r#7")])
// Already-known review: no new event
oldPulse.toReview = [pr]
newPulse.toReview = [pr]
let noEvents = GitHubPulse.events(old: oldPulse, new: newPulse)
check("known review → no event", noEvents.isEmpty)
}
// ── GitHubPulse.parse — duplicate ids ────────────────────────────────────
print("GitHubPulse.parse — duplicate ids")
do {
// JSON with PR #42 duplicated: should keep only first occurrence
let dupJSON: Data = """
{
"data": {
"viewer": {
"login": "testuser",
"pullRequests": {
"nodes": [
{
"number": 42, "title": "Add feature",
"url": "https://github.com/testuser/myrepo/pull/42",
"isDraft": false, "reviewDecision": "APPROVED",
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
"commits": {"nodes": [{"commit": {"statusCheckRollup": {"state": "SUCCESS"}}}]}
},
{
"number": 42, "title": "Duplicate entry",
"url": "https://github.com/testuser/myrepo/pull/42",
"isDraft": true, "reviewDecision": null,
"repository": {"nameWithOwner": "testuser/myrepo", "url": "https://github.com/testuser/myrepo"},
"commits": {"nodes": [{"commit": {"statusCheckRollup": {"state": "FAILURE"}}}]}
}
]
},
"repositories": {"nodes": []}
},
"reviewRequested": {"issueCount": 0, "nodes": []}
}
}
""".data(using: .utf8)!
let pulse = GitHubPulse.parse(dupJSON)
check("dup PR → 1 myPR", pulse?.myPRs.count == 1)
check("dup PR → keeps first title", pulse?.myPRs[0].title == "Add feature")
check("dup PR → keeps first ci", pulse?.myPRs[0].ci == .success)
}
// ── GitHubPulse.events — duplicate ids in old pulse ───────────────────
print("GitHubPulse.events — duplicate-safe Dictionary")
do {
// OLD has duplicate ids (uniquingKeysWith keeps first); NEW has one entry → fires once
var oldPulse = GitHubPulse.parse(emptyJSON)!
let pr1 = GitHubPR(id: "r/p#1", title: "T", url: "", repo: "r/p",
number: 1, isDraft: false, ci: .pending, review: .unknown)
oldPulse.myPRs = [pr1, pr1] // intentional duplicate in old
var newPulse = GitHubPulse.parse(emptyJSON)!
var pr1Passed = pr1; pr1Passed.ci = .success
newPulse.myPRs = [pr1Passed] // single entry in new
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("dup old ids → no crash", true)
check("dup old ids → ciPassed fires once",
events.filter { if case .ciPassed = $0 { return true }; return false }.count == 1)
}
// ── GitHubPulse.events — headSha: new PR already green ───────────────
print("GitHubPulse.events — headSha: new PR already green")
do {
let old = GitHubPulse.parse(emptyJSON)!
var newPulse = old
newPulse.myPRs = [GitHubPR(id: "r/p#10", title: "T", url: "", repo: "r/p",
number: 10, isDraft: false, ci: .success, review: .unknown,
headSha: "abc111")]
let events = GitHubPulse.events(old: old, new: newPulse)
check("new PR green → ciPassed", events == [.ciPassed(prId: "r/p#10")])
}
// ── GitHubPulse.events — headSha: new PR pending → nothing ───────────
print("GitHubPulse.events — headSha: new PR pending → nothing")
do {
let old = GitHubPulse.parse(emptyJSON)!
var newPulse = old
newPulse.myPRs = [GitHubPR(id: "r/p#11", title: "T", url: "", repo: "r/p",
number: 11, isDraft: false, ci: .pending, review: .unknown,
headSha: "abc222")]
let events = GitHubPulse.events(old: old, new: newPulse)
check("new PR pending → no events", events.isEmpty)
}
// ── GitHubPulse.events — headSha: new commit already red ─────────────
print("GitHubPulse.events — headSha: new commit already red → ciFailed")
do {
var oldPulse = GitHubPulse.parse(emptyJSON)!
oldPulse.myPRs = [GitHubPR(id: "r/p#12", title: "T", url: "", repo: "r/p",
number: 12, isDraft: false, ci: .success, review: .unknown,
headSha: "sha-old")]
var newPulse = oldPulse
newPulse.myPRs[0].ci = .failure
newPulse.myPRs[0].headSha = "sha-new"
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("new commit red → ciFailed", events == [.ciFailed(prId: "r/p#12")])
}
// ── GitHubPulse.events — headSha: same sha success→success → nothing ─
print("GitHubPulse.events — headSha: same sha success→success → nothing")
do {
var oldPulse = GitHubPulse.parse(emptyJSON)!
oldPulse.myPRs = [GitHubPR(id: "r/p#13", title: "T", url: "", repo: "r/p",
number: 13, isDraft: false, ci: .success, review: .unknown,
headSha: "same")]
let newPulse = oldPulse // identical SHA and CI
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("same sha success→success → no event", events.isEmpty)
}
// ── GitHubPulse.events — headSha: new commit on main red → mainFailed ─
print("GitHubPulse.events — headSha: new commit on main red → mainFailed")
do {
var oldPulse = GitHubPulse.parse(emptyJSON)!
oldPulse.mainCI = [GitHubRepoCI(repo: "a/b", url: "", branch: "main",
ci: .success, headSha: "sha-old")]
var newPulse = oldPulse
newPulse.mainCI[0].ci = .failure
newPulse.mainCI[0].headSha = "sha-new"
let events = GitHubPulse.events(old: oldPulse, new: newPulse)
check("new commit on main red → mainFailed", events == [.mainFailed(repo: "a/b")])
}
// ── GitHubPulse.isStale ───────────────────────────────────────────────
print("GitHubPulse.isStale")
do {
let now = Date()
check("nil fetchedAt → stale",
GitHubPulse.isStale(fetchedAt: nil, now: now, maxAge: 60))
check("fresh (same instant) → not stale",
!GitHubPulse.isStale(fetchedAt: now, now: now, maxAge: 60))
check("61 s ago → stale",
GitHubPulse.isStale(fetchedAt: now.addingTimeInterval(-61), now: now, maxAge: 60))
}
// ── finish ─────────────────────────────────────────────────────────────
if failures == 0 {
print("\nAll tests passed.")
exit(0)
} else {
print("\n\(failures) test(s) failed.")
exit(1)
}
}
}