refactor(rules): 统一来源类型映射并移除测试专用加载参数

This commit is contained in:
Qiu Yuzhou
2026-10-04 02:41:25 +08:00
parent 759fbfbb8c
commit 0aa0dc78c4
14 changed files with 137 additions and 102 deletions
+2 -10
View File
@@ -31,15 +31,7 @@ final class RulesWorkflow: ObservableObject {
analysisDelay: @escaping @Sendable () async throws -> Void = {
try await Task.sleep(for: .milliseconds(150))
},
builtinSnapshots: BuiltinRuleSnapshots? = nil,
loadBuiltin: @escaping @Sendable (RulesSource) throws -> RuleSnapshot = { source in
switch source {
case .geolocationCN: try BuiltinRuleCatalog.loadGeolocationCN()
case .chinaIPv4: try BuiltinRuleCatalog.loadChinaIPv4()
case .gfwlist: try BuiltinRuleCatalog.loadGFWList()
case .custom, .fixed: throw RuleSnapshotError.missing
}
}
builtinSnapshots: BuiltinRuleSnapshots = BuiltinRuleSnapshots()
) {
if let loadDocument {
self.loadDocument = loadDocument
@@ -52,7 +44,7 @@ final class RulesWorkflow: ObservableObject {
self.feedbackDelay = feedbackDelay
self.analysisDelay = analysisDelay
self.loadCustom = loadCustom ?? { try CustomRuleStore().load() }
self.builtinSnapshots = builtinSnapshots ?? BuiltinRuleSnapshots(loader: loadBuiltin)
self.builtinSnapshots = builtinSnapshots
}
func refresh(retryFailedSources: Bool = false) async {
@@ -19,7 +19,9 @@ struct BuiltinRuleCatalog {
try loadSnapshot(named: "gfwlist", from: bundle)
}
private static func loadSnapshot(named name: String, from bundle: Bundle) throws -> RuleSnapshot {
fileprivate static func loadSnapshot(named name: String, from bundle: Bundle) throws
-> RuleSnapshot
{
// folder reference 保留 `rules/<source>/snapshot.json` 目录结构。
guard
let url = bundle.url(
@@ -62,12 +64,10 @@ actor BuiltinRuleSnapshots {
init(bundle: Bundle = .main, loader: Loader? = nil) {
self.loader =
loader ?? { source in
switch source {
case .geolocationCN: try BuiltinRuleCatalog.loadGeolocationCN(from: bundle)
case .chinaIPv4: try BuiltinRuleCatalog.loadChinaIPv4(from: bundle)
case .gfwlist: try BuiltinRuleCatalog.loadGFWList(from: bundle)
case .fixed, .custom: throw RuleSnapshotError.missing
guard let kind = source.sourceKind, kind != .custom else {
throw RuleSnapshotError.missing
}
return try BuiltinRuleCatalog.loadSnapshot(named: kind.rawValue, from: bundle)
}
}
@@ -82,12 +82,8 @@ actor BuiltinRuleSnapshots {
id: UUID(),
task: Task.detached(priority: .userInitiated) {
let snapshot = try loader(source)
let kind: RuleSourceKind
switch source {
case .geolocationCN: kind = .geolocationCN
case .chinaIPv4: kind = .chinaIPv4
case .gfwlist: kind = .gfwlist
case .custom, .fixed: throw RuleSnapshotError.missing
guard let kind = source.sourceKind, kind != .custom else {
throw RuleSnapshotError.missing
}
guard snapshot.metadata.source.kind == kind
else { throw RuleSnapshotError.corrupt(detail: "Unexpected rule source") }
@@ -11,6 +11,17 @@ struct RulesQuery: Equatable, Sendable {
enum RulesSource: String, CaseIterable, Identifiable, Sendable {
case custom, geolocationCN, chinaIPv4, gfwlist, fixed
var id: Self { self }
/// Fixed policy has no snapshot metadata kind.
var sourceKind: RuleSourceKind? {
switch self {
case .custom: .custom
case .geolocationCN: .geolocationCN
case .chinaIPv4: .chinaIPv4
case .gfwlist: .gfwlist
case .fixed: nil
}
}
}
struct RulesSourceSnapshot: Equatable, Identifiable, Sendable {
@@ -89,13 +89,7 @@ private struct RulesCollectionInput: Sendable {
) {
do {
let snapshot = try loadBuiltin(source)
let expected: RuleSourceKind
switch source {
case .geolocationCN: expected = .geolocationCN
case .chinaIPv4: expected = .chinaIPv4
case .gfwlist: expected = .gfwlist
case .custom, .fixed: expected = .custom
}
guard let expected = source.sourceKind else { throw RuleSnapshotError.missing }
guard snapshot.metadata.source.kind == expected
else { throw RuleSnapshotError.corrupt(detail: "Unexpected rule source") }
let candidates = snapshot.rules
@@ -1,4 +1,5 @@
import Foundation
import Testing
import XCTest
@testable import ShadowsocksX_NG2
@@ -255,3 +256,25 @@ private final class SharedRuleSourceLoader: @unchecked Sendable {
return rulesFixture(source)
}
}
@Test func builtinSnapshotsRejectNonBuiltinSources() async {
let snapshots = BuiltinRuleSnapshots(loader: { _ in rulesFixture(.custom) })
for source in [RulesSource.custom, .fixed] {
do {
_ = try await snapshots.load(source)
Issue.record("Non-builtin source must not load a snapshot")
} catch {
#expect(error as? RuleSnapshotError == .missing)
}
}
}
@Test func builtinSnapshotsRejectMismatchedMetadata() async {
let snapshots = BuiltinRuleSnapshots(loader: { _ in rulesFixture(.chinaIPv4) })
do {
_ = try await snapshots.load(.geolocationCN)
Issue.record("Mismatched source metadata must be rejected")
} catch {
#expect(error as? RuleSnapshotError == .corrupt(detail: "Unexpected rule source"))
}
}
@@ -21,7 +21,9 @@ final class CustomRuleDeletionWorkflowTests: XCTestCase {
saved = $0
return RuleDocumentCommit(outcome: .saved, document: saved)
},
loadBuiltin: { rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : []) })
builtinSnapshots: BuiltinRuleSnapshots(loader: {
rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : [])
}))
await workflow.refresh()
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
XCTAssertEqual(workflow.deletableSelection, [custom.id, other.id])
@@ -57,7 +59,7 @@ extension CustomRuleDeletionWorkflowTests {
commits += 1
saved = $0
return RuleDocumentCommit(outcome: .saved, document: saved)
}, loadBuiltin: { rulesFixture($0) })
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
var query = workflow.snapshot.query
@@ -86,7 +88,7 @@ extension CustomRuleDeletionWorkflowTests {
commitDocument: {
commits += 1
return RuleDocumentCommit(outcome: .saved, document: $0)
}, loadBuiltin: { rulesFixture($0) })
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
@@ -114,7 +116,7 @@ extension CustomRuleDeletionWorkflowTests {
started.fulfill()
}
return RuleDocumentCommit(outcome: .persistenceFailed, document: original)
}, loadBuiltin: { rulesFixture($0) })
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
@@ -143,7 +145,7 @@ extension CustomRuleDeletionWorkflowTests {
commitDocument: {
commits += 1
return RuleDocumentCommit(outcome: .saved, document: $0)
}, loadBuiltin: { rulesFixture($0) })
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
@@ -8,7 +8,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
let workflow = RulesWorkflow(
loadDocument: { CustomRuleDocument(rules: []) },
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let version = workflow.snapshot.version
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
@@ -40,7 +40,9 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
saved = $0
return RuleDocumentCommit(outcome: .saved, document: saved)
},
loadBuiltin: { rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : []) })
builtinSnapshots: BuiltinRuleSnapshots(loader: {
rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : [])
}))
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: original.id))
let unchanged = await workflow.previewCustomRule(draft)
@@ -71,7 +73,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
let workflow = RulesWorkflow(
loadDocument: { CustomRuleDocument(rules: []) },
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
draft.kind = .cidr
@@ -100,7 +102,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
commits += 1
return RuleDocumentCommit(outcome: .saved, document: $0)
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
draft.kind = .ipAddress
@@ -134,7 +136,9 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
CustomRuleDocument(rules: [], disabledIdentities: [builtin.identity, orphan])
},
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
loadBuiltin: { rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : []) })
builtinSnapshots: BuiltinRuleSnapshots(loader: {
rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : [])
}))
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
draft.content = ".MERGE.Example"
@@ -163,7 +167,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
let workflow = RulesWorkflow(
loadDocument: { CustomRuleDocument(rules: []) },
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
draft.action = .direct
@@ -200,7 +204,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
let workflow = RulesWorkflow(
loadDocument: { CustomRuleDocument(rules: [broad, proxy]) },
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
draft.kind = .domainExact
@@ -239,7 +243,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
commits += 1
return RuleDocumentCommit(outcome: .saved, document: $0)
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
var discarded = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: rule.id))
discarded.content = "discarded.example"
@@ -274,7 +278,7 @@ extension CustomRuleEditingWorkflowTests {
}
return RuleDocumentCommit(outcome: .persistenceFailed, document: original)
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let version = workflow.snapshot.version
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
@@ -304,7 +308,7 @@ extension CustomRuleEditingWorkflowTests {
let workflow = RulesWorkflow(
loadDocument: { original },
commitDocument: { RuleDocumentCommit(outcome: outcome, document: $0) },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
XCTAssertNil(workflow.makeCustomRuleDraft())
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: rule.id))
@@ -319,7 +323,7 @@ extension CustomRuleEditingWorkflowTests {
let incomplete = RulesWorkflow(
loadDocument: { throw CustomRuleStoreError.corrupt(detail: "fixture error") },
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await incomplete.refresh()
XCTAssertNil(incomplete.makeCustomRuleDraft())
let refused = await incomplete.saveCustomRule(draft)
@@ -339,7 +343,7 @@ extension CustomRuleEditingWorkflowTests {
commits += 1
return RuleDocumentCommit(outcome: .saved, document: $0)
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: original.id))
draft.kind = .cidr
@@ -6,7 +6,8 @@ import XCTest
final class OfflineRulesWorkflowTests: XCTestCase {
func testAddressTestUsesCompleteSavedCollectionDespiteBrowsingFilters() async throws {
let rule = CustomRule(action: .proxy, match: try RuleMatch(domainSuffix: "example.com"))
let workflow = RulesWorkflow(loadCustom: { [rule] }, loadBuiltin: { rulesFixture($0) })
let workflow = RulesWorkflow(
loadCustom: { [rule] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.query(RulesQuery(search: "hidden", action: .direct, source: .chinaIPv4))
XCTAssertTrue(workflow.snapshot.rows.isEmpty)
@@ -19,7 +20,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
XCTAssertTrue(result.domainWithoutDNS)
}
func testInvalidAndIncompleteCollectionsExposeOperationErrors() async throws {
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { rulesFixture($0) })
let workflow = RulesWorkflow(
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
workflow.setTestTarget("example.com")
await workflow.testAddress()
XCTAssertEqual(workflow.snapshot.addressTest.failure, .incompleteCollection)
@@ -29,7 +31,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
XCTAssertEqual(workflow.snapshot.addressTest.failure, .invalidTarget)
XCTAssertNil(workflow.snapshot.addressTest.result)
let incomplete = RulesWorkflow(
loadCustom: { [] }, loadBuiltin: { _ in throw RuleSnapshotError.missing })
loadCustom: { [] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { _ in throw RuleSnapshotError.missing }))
await incomplete.refresh()
incomplete.setTestTarget("localhost")
await incomplete.testAddress()
@@ -44,7 +47,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
let proxy = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "example.com"))
try store.save([proxy])
let workflow = RulesWorkflow(
loadCustom: { try store.load() }, loadBuiltin: { rulesFixture($0) })
loadCustom: { try store.load() },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.setTestTarget("example.com")
await workflow.testAddress()
@@ -66,7 +70,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
func testClearingTargetOrStartingRefreshPreventsLateTestPublication() async throws {
let rule = CustomRule(action: .proxy, match: try RuleMatch(domainSuffix: "example.com"))
let workflow = RulesWorkflow(loadCustom: { [rule] }, loadBuiltin: { rulesFixture($0) })
let workflow = RulesWorkflow(
loadCustom: { [rule] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.setTestTarget("example.com")
await workflow.testAddress()
@@ -102,7 +107,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
CustomRule(action: .proxy, match: try RuleMatch(domainExact: "proxy.example")),
CustomRule(action: .direct, match: try RuleMatch(domainExact: "direct.example")),
]
let workflow = RulesWorkflow(loadCustom: { rules }, loadBuiltin: { rulesFixture($0) })
let workflow = RulesWorkflow(
loadCustom: { rules }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
for mode in [ProxyModeKind.rule, .global, .direct] {
for defaultAction in RuleDefaultAction.allCases {
@@ -18,7 +18,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
return RuleDocumentCommit(outcome: .saved, document: $0)
},
analysisDelay: { await gate.wait() },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.query(RulesQuery(source: .custom))
let originalOrder = workflow.snapshot.rows.map(\.id)
@@ -69,7 +69,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
}
},
analysisDelay: { await gate.wait() },
loadBuiltin: { source in
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
if source == .geolocationCN {
return rulesFixture(
source,
@@ -79,7 +79,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
])
}
return rulesFixture(source)
})
}))
await workflow.refresh()
XCTAssertTrue(workflow.snapshot.isComplete)
XCTAssertTrue(workflow.snapshot.sources.contains { $0.id == .geolocationCN })
@@ -109,10 +109,10 @@ final class RuleEnablementWorkflowTests: XCTestCase {
return CustomRuleDocument(rules: [rule])
},
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
loadBuiltin: { source in
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
reads.recordSource()
return rulesFixture(source)
})
}))
await workflow.refresh()
await workflow.setEnabled(false, identities: [rule.identity])
XCTAssertEqual(reads.documentCount, 1)
@@ -147,7 +147,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
outcome: .persistenceFailed, document: try? store.loadDocument())
}
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let pending = Task { await workflow.setEnabled(false, identities: [rule.identity]) }
await fulfillment(of: [started], timeout: 3)
@@ -181,7 +181,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
commits += 1
return RuleDocumentCommit(outcome: .persistenceFailed, document: try? store.loadDocument())
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let version = workflow.snapshot.version
await workflow.setEnabled(false, identities: [rule.identity])
@@ -18,7 +18,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
return RuleDocumentCommit(outcome: .saved, document: nil)
},
feedbackDelay: { XCTFail("An incomplete collection must not start the success timer") },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let rows = workflow.snapshot.rows
await workflow.setEnabled(false, identities: [rule.identity])
@@ -46,7 +46,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
saveFixture(document, to: store, outcome: .saved)
},
feedbackDelay: { await clock.wait() },
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let fixed = try XCTUnwrap(workflow.snapshot.rows.first { $0.isFixed }?.identity)
await workflow.setEnabled(true, identities: [disabled.identity, enabled.identity, fixed])
@@ -80,7 +80,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
await gate.wait()
return saveFixture(document, to: store, outcome: .saved)
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let pending = Task { await workflow.setEnabled(false, identities: [rule.identity]) }
await fulfillment(of: [reloadStarted], timeout: 3)
@@ -121,7 +121,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
await clock.wait()
timerReturned.fulfill()
},
loadBuiltin: { rulesFixture($0) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
await workflow.setEnabled(false, identities: [rule.identity])
await fulfillment(of: [timerStarted], timeout: 3)
@@ -6,7 +6,8 @@ import XCTest
final class RulesPresentationTests: XCTestCase {
func testReportIsAnExplicitSnapshotUnaffectedByQueryOrRefresh() async throws {
let loader = ReportFixtureLoader()
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { loader.load($0) })
let workflow = RulesWorkflow(
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { loader.load($0) }))
await workflow.refresh()
XCTAssertTrue(workflow.openSourceReport(.gfwlist))
let opened = try XCTUnwrap(workflow.reportSource)
@@ -26,7 +27,8 @@ final class RulesPresentationTests: XCTestCase {
let narrow = CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.example.com"))
let independent = CustomRule(action: .direct, match: try RuleMatch(domainExact: "other.net"))
let workflow = RulesWorkflow(
loadCustom: { [broad, narrow, independent] }, loadBuiltin: { rulesFixture($0) })
loadCustom: { [broad, narrow, independent] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
XCTAssertNil(workflow.selectedRelationshipRow)
workflow.select([.rule(narrow.identity)])
@@ -45,7 +47,8 @@ final class RulesPresentationTests: XCTestCase {
let narrow = CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.example.net"))
let local = CustomRule(action: .direct, match: try RuleMatch(domainExact: "device.local"))
let workflow = RulesWorkflow(
loadCustom: { [broad, narrow, local] }, loadBuiltin: { rulesFixture($0) })
loadCustom: { [broad, narrow, local] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.select([.rule(narrow.identity)])
let absorbed = try XCTUnwrap(workflow.selectedRelationshipRow)
@@ -10,7 +10,8 @@ final class RulesWorkflowCoverageTests: XCTestCase {
let duplicate = CustomRule(
action: .direct, match: try RuleMatch(domainExact: "www.example.com"))
let workflow = RulesWorkflow(
loadCustom: { [broad, narrow, duplicate] }, loadBuiltin: { rulesFixture($0) })
loadCustom: { [broad, narrow, duplicate] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.query(RulesQuery(action: .direct, source: .custom))
let broadRow = try XCTUnwrap(workflow.snapshot.rows.first { $0.identity == broad.identity })
@@ -30,7 +31,8 @@ final class RulesWorkflowCoverageTests: XCTestCase {
let lower = CustomRule(action: .direct, match: try RuleMatch(ipv4CIDR: "8.8.8.0/25"))
let upper = CustomRule(action: .direct, match: try RuleMatch(ipv4CIDR: "8.8.8.128/25"))
let workflow = RulesWorkflow(
loadCustom: { [proxy, lower, upper] }, loadBuiltin: { rulesFixture($0) })
loadCustom: { [proxy, lower, upper] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.query(RulesQuery(action: .proxy, source: .custom))
let row = try XCTUnwrap(workflow.snapshot.rows.first)
@@ -51,10 +53,10 @@ final class RulesWorkflowCoverageTests: XCTestCase {
let nationalDirect = CustomRule(action: .direct, match: .domainSuffix("cn"))
let workflow = RulesWorkflow(
loadCustom: { [localDirect, nationalDirect] },
loadBuiltin: { requested in
builtinSnapshots: BuiltinRuleSnapshots(loader: { requested in
rulesFixture(
requested, rules: requested == .gfwlist ? [simple, simpleSuffix, localProxy] : [])
})
}))
await workflow.refresh()
workflow.query(RulesQuery(source: .gfwlist))
let simpleRow = try XCTUnwrap(workflow.snapshot.rows.first { $0.identity == simple.identity })
@@ -79,9 +81,9 @@ final class RulesWorkflowCoverageTests: XCTestCase {
let proxy = ProxyRule(action: .proxy, match: try RuleMatch(domainExact: "cn"))
let workflow = RulesWorkflow(
loadCustom: { [direct] },
loadBuiltin: { requested in
builtinSnapshots: BuiltinRuleSnapshots(loader: { requested in
rulesFixture(requested, rules: requested == .gfwlist ? [proxy] : [])
})
}))
await workflow.refresh()
workflow.query(RulesQuery(source: .custom))
let row = try XCTUnwrap(workflow.snapshot.rows.first)
@@ -5,7 +5,8 @@ import XCTest
@MainActor
final class RulesWorkflowPublicationTests: XCTestCase {
func testNativeControlsWritingTheSameQueryDoNotRepublishThePage() async {
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { rulesFixture($0) })
let workflow = RulesWorkflow(
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.query(RulesQuery(source: .gfwlist))
var changes = 0
@@ -16,7 +17,8 @@ final class RulesWorkflowPublicationTests: XCTestCase {
}
func testNativeControlsWritingTheSameVisibleSelectionDoNotRepublishThePage() async {
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { rulesFixture($0) })
let workflow = RulesWorkflow(
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.select([.noDotHostname])
var changes = 0
+28 -28
View File
@@ -27,14 +27,14 @@ final class RulesWorkflowTests: XCTestCase {
outcome: .persistenceFailed, document: try? store.loadDocument())
}
},
loadBuiltin: { source in
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
guard source == .gfwlist else { return rulesFixture(source) }
return rulesFixture(
source,
rules: [
ProxyRule(action: .proxy, match: broad.match)
])
})
}))
await workflow.refresh()
let version = workflow.snapshot.version
let covered = try XCTUnwrap(workflow.snapshot.rows.first { $0.identity == narrow.identity })
@@ -72,7 +72,9 @@ final class RulesWorkflowTests: XCTestCase {
rules: [ProxyRule(action: .direct, match: match)])
let workflow = RulesWorkflow(
loadCustom: { [custom] },
loadBuiltin: { source in source == .geolocationCN ? snapshot : rulesFixture(source) })
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
source == .geolocationCN ? snapshot : rulesFixture(source)
}))
await workflow.refresh()
workflow.query(RulesQuery(search: "EXAMPLE", action: .direct, source: .geolocationCN))
let row = try XCTUnwrap(workflow.snapshot.rows.first)
@@ -85,9 +87,9 @@ final class RulesWorkflowTests: XCTestCase {
func testWrongBuiltinSourceIsAnIncompleteCollectionRatherThanMislabeledRules() async {
let workflow = RulesWorkflow(
loadCustom: { [] },
loadBuiltin: { _ in
builtinSnapshots: BuiltinRuleSnapshots(loader: { _ in
rulesFixture(.geolocationCN)
})
}))
await workflow.refresh()
XCTAssertFalse(workflow.snapshot.isComplete)
XCTAssertEqual(workflow.snapshot.issues.count, 2)
@@ -97,7 +99,8 @@ final class RulesWorkflowTests: XCTestCase {
let direct = CustomRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
let proxy = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "outside.net"))
let workflow = RulesWorkflow(
loadCustom: { [direct, proxy] }, loadBuiltin: { rulesFixture($0) })
loadCustom: { [direct, proxy] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.select([.rule(direct.identity), .rule(proxy.identity), .noDotHostname])
workflow.query(RulesQuery(search: "EXAMPLE", action: .direct, source: .custom))
@@ -114,7 +117,8 @@ final class RulesWorkflowTests: XCTestCase {
CustomRule(action: .proxy, match: try RuleMatch(domainExact: "z.net")),
CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.net")),
]
let workflow = RulesWorkflow(loadCustom: { entries }, loadBuiltin: { rulesFixture($0) })
let workflow = RulesWorkflow(
loadCustom: { entries }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let version = workflow.snapshot.version
workflow.query(RulesQuery(source: .custom))
@@ -134,10 +138,10 @@ final class RulesWorkflowTests: XCTestCase {
func testCorruptUserDocumentAndMissingBuiltinStayDistinctFromEmptySearch() async {
let workflow = RulesWorkflow(
loadCustom: { throw CustomRuleStoreError.corrupt(detail: "fixture") },
loadBuiltin: { source in
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
if source == .chinaIPv4 { throw RuleSnapshotError.missing }
return rulesFixture(source)
})
}))
await workflow.refresh()
XCTAssertFalse(workflow.snapshot.isComplete)
XCTAssertEqual(
@@ -154,14 +158,7 @@ final class RulesWorkflowTests: XCTestCase {
let bundle = AppArtifact.bundle
let workflow = RulesWorkflow(
loadCustom: { [] },
loadBuiltin: { source in
switch source {
case .geolocationCN: try BuiltinRuleCatalog.loadGeolocationCN(from: bundle)
case .chinaIPv4: try BuiltinRuleCatalog.loadChinaIPv4(from: bundle)
case .gfwlist: try BuiltinRuleCatalog.loadGFWList(from: bundle)
case .custom, .fixed: throw RuleSnapshotError.missing
}
})
builtinSnapshots: BuiltinRuleSnapshots(bundle: bundle))
await workflow.refresh()
XCTAssertTrue(workflow.snapshot.isComplete, "\(workflow.snapshot.issues)")
XCTAssertEqual(workflow.snapshot.sources.count, 5)
@@ -185,7 +182,8 @@ final class RulesWorkflowTests: XCTestCase {
let new = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "new.net"))
try store.save([old])
let workflow = RulesWorkflow(
loadCustom: { try store.load() }, loadBuiltin: { rulesFixture($0) })
loadCustom: { try store.load() },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.query(RulesQuery(source: .custom))
workflow.select([.rule(old.identity)])
@@ -215,7 +213,8 @@ final class RulesWorkflowTests: XCTestCase {
let firstRead = XCTestExpectation(description: "first read")
let barrier = RulesReadBarrier(firstRead: firstRead)
let workflow = RulesWorkflow(
loadCustom: { try barrier.read() }, loadBuiltin: { rulesFixture($0) })
loadCustom: { try barrier.read() },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
let older = Task { await workflow.refresh() }
await fulfillment(of: [firstRead], timeout: 3)
defer { barrier.release.signal() }
@@ -237,8 +236,12 @@ extension RulesWorkflowTests {
func testCandidatePermutationKeepsVersionAndCoverageWhilePreservingInputOrder() async throws {
let broad = CustomRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
let narrow = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "www.example.com"))
let forward = RulesWorkflow(loadCustom: { [broad, narrow] }, loadBuiltin: { rulesFixture($0) })
let reverse = RulesWorkflow(loadCustom: { [narrow, broad] }, loadBuiltin: { rulesFixture($0) })
let forward = RulesWorkflow(
loadCustom: { [broad, narrow] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
let reverse = RulesWorkflow(
loadCustom: { [narrow, broad] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await forward.refresh()
await reverse.refresh()
XCTAssertEqual(forward.snapshot.version, reverse.snapshot.version)
@@ -255,7 +258,8 @@ extension RulesWorkflowTests {
let exact = CustomRule(action: .direct, match: try RuleMatch(domainExact: "example.com"))
let suffix = CustomRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
let workflow = RulesWorkflow(
loadCustom: { [suffix, exact] }, loadBuiltin: { rulesFixture($0) })
loadCustom: { [suffix, exact] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
let version = workflow.snapshot.version
let identities = workflow.snapshot.rows.map(\.identity)
@@ -292,12 +296,8 @@ extension RulesWorkflowTests {
func rulesFixture(
_ source: RulesSource, rules: [ProxyRule] = []
) -> RuleSnapshot {
let kind: RuleSourceKind
switch source {
case .geolocationCN: kind = .geolocationCN
case .chinaIPv4: kind = .chinaIPv4
case .gfwlist: kind = .gfwlist
case .custom, .fixed: kind = .custom
guard let kind = source.sourceKind else {
preconditionFailure("Fixed policy has no rule snapshot")
}
return RuleSnapshot(
metadata: RuleSnapshotMetadata(