mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:54:50 +00:00
Each removed assertion holds by construction and can never fail: - jsonData() compared with itself cannot detect cross-run byte stability; bindAddress and mode are literal-returning computed properties - RuleAction/RuleSourceKind allCases assertions pin synthesized enum order (neither allCases list is used in production); the persisted action tokens stay pinned by the kept rawValue assertions and source-kind tokens by fixture decoding through RuleSnapshotStore - memberwise-constructor echo in RuleSourceIdentity, fixture-guaranteed name == id rows, a share draft field pair built from one string, and a local struct compared to its own assignment - the synthesized-Codable round trip through literal JSONEncoder/JSONDecoder The duplicate-import assertion becomes a real check: Set of node ids instead of a count that restated roots.count, which is what its comment claims. Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
271 lines
11 KiB
Swift
271 lines
11 KiB
Swift
import Testing
|
||
import XCTest
|
||
|
||
@testable import ShadowsocksX_NG2
|
||
|
||
/// CatalogTreeSnapshot 树投影的纯单元测试:折叠可见行与服务器叶子计数。
|
||
/// 直接构造 snapshot(memberwise init),不经工作流。
|
||
final class CatalogTreeProjectionTests: XCTestCase {
|
||
private func server(_ id: String) -> CatalogTreeNode {
|
||
CatalogTreeNode(
|
||
id: NodeID(rawValue: id), name: id, isGroup: false, source: .manual, parentID: nil,
|
||
createdAt: nil, updatedAt: nil,
|
||
invalidReasons: [], childCount: 0, subtreeNodeCount: 0, invalidDescendantCount: 0,
|
||
children: nil)
|
||
}
|
||
|
||
private func group(_ id: String, children: [CatalogTreeNode]) -> CatalogTreeNode {
|
||
CatalogTreeNode(
|
||
id: NodeID(rawValue: id), name: id, isGroup: true, source: .manual, parentID: nil,
|
||
createdAt: nil, updatedAt: nil,
|
||
invalidReasons: [], childCount: children.count,
|
||
subtreeNodeCount: children.reduce(0) { $0 + 1 + $1.subtreeNodeCount },
|
||
invalidDescendantCount: 0, children: children)
|
||
}
|
||
|
||
/// 两层嵌套 + 根叶子 + 空组:覆盖深度、子树跳过与空组语义。
|
||
private func nestedTree() -> CatalogTreeSnapshot {
|
||
CatalogTreeSnapshot(roots: [
|
||
server("root-server"),
|
||
group(
|
||
"outer",
|
||
children: [
|
||
group("inner", children: [server("deep-leaf")]),
|
||
server("outer-leaf"),
|
||
]),
|
||
group("empty", children: []),
|
||
])
|
||
}
|
||
|
||
// MARK: - 可见行折叠投影
|
||
|
||
func testVisibleRowsWalkDepthFirstWithDepth() {
|
||
let rows = nestedTree().visibleRows(collapsed: [])
|
||
XCTAssertEqual(
|
||
rows.map(\.id.rawValue),
|
||
["root-server", "outer", "inner", "deep-leaf", "outer-leaf", "empty"])
|
||
XCTAssertEqual(rows.map(\.depth), [0, 0, 1, 2, 1, 0])
|
||
}
|
||
|
||
func testVisibleRowsSkipCollapsedSubtrees() {
|
||
let tree = nestedTree()
|
||
XCTAssertEqual(
|
||
tree.visibleRows(collapsed: [NodeID(rawValue: "outer")]).map(\.id.rawValue),
|
||
["root-server", "outer", "empty"], "收起组的子树不出现,组行自身保留可再展开")
|
||
XCTAssertEqual(
|
||
tree.visibleRows(collapsed: [NodeID(rawValue: "inner")]).map(\.id.rawValue),
|
||
["root-server", "outer", "inner", "outer-leaf", "empty"])
|
||
}
|
||
|
||
func testVisibleRowsTreatUnknownAndLeafIDsAsInert() {
|
||
let collapsed: Set<NodeID> = [NodeID(rawValue: "nope"), NodeID(rawValue: "root-server")]
|
||
XCTAssertEqual(
|
||
nestedTree().visibleRows(collapsed: collapsed).map(\.id.rawValue),
|
||
["root-server", "outer", "inner", "deep-leaf", "outer-leaf", "empty"])
|
||
}
|
||
|
||
// MARK: - 服务器叶子计数
|
||
|
||
func testServerLeafCountCountsLeavesAcrossNesting() {
|
||
XCTAssertEqual(nestedTree().serverLeafCount, 3)
|
||
XCTAssertEqual(CatalogTreeSnapshot(roots: []).serverLeafCount, 0)
|
||
}
|
||
}
|
||
|
||
struct CatalogTreeTimestampProjectionTests {
|
||
@Test
|
||
func treeExposesCommittedCreationAndModificationTimes() throws {
|
||
var catalog = ConfigurationCatalog()
|
||
let created = Date(timeIntervalSince1970: 100)
|
||
let modified = Date(timeIntervalSince1970: 200)
|
||
let group = try catalog.addGroup("原名", now: created)
|
||
try catalog.renameGroup(group, to: "新名", now: modified)
|
||
let tree = CatalogTreeSnapshot.build(
|
||
from: catalog, plugins: NoManagedPluginProvider())
|
||
|
||
#expect(tree.node(withID: group)?.createdAt == created)
|
||
#expect(tree.node(withID: group)?.updatedAt == modified)
|
||
}
|
||
}
|
||
|
||
struct SubscriptionRootGroupClassificationTests {
|
||
private func node(
|
||
_ id: String, source: NodeSource, parentID: NodeID?, isGroup: Bool = true
|
||
) -> CatalogTreeNode {
|
||
CatalogTreeNode(
|
||
id: NodeID(rawValue: id), name: id, isGroup: isGroup, source: source, parentID: parentID,
|
||
createdAt: nil, updatedAt: nil, invalidReasons: [], childCount: 0,
|
||
subtreeNodeCount: 0, invalidDescendantCount: 0, children: isGroup ? [] : nil)
|
||
}
|
||
|
||
@Test
|
||
func onlyRootSubscriptionGroupsQualify() {
|
||
let rootID = NodeID(rawValue: "subscription-root")
|
||
let root = node("subscription-root", source: .subscription, parentID: nil)
|
||
let nested = node("nested-subscription-group", source: .subscription, parentID: rootID)
|
||
let manual = node("manual-group", source: .manual, parentID: nil)
|
||
let server = node("subscription-server", source: .subscription, parentID: nil, isGroup: false)
|
||
|
||
#expect(root.isRootSubscriptionGroup)
|
||
#expect(!nested.isRootSubscriptionGroup)
|
||
#expect(!manual.isRootSubscriptionGroup)
|
||
#expect(!server.isRootSubscriptionGroup)
|
||
}
|
||
}
|
||
|
||
struct ServerTableDirectoryOrderTests {
|
||
@Test
|
||
func hierarchyKeepsDirectoryOrderAndCollapseHidesOnlyThatSubtree() throws {
|
||
var catalog = ConfigurationCatalog()
|
||
let outer = try catalog.addGroup("Z 外层")
|
||
let root = try catalog.addGroup("A 根层")
|
||
let childZ = try catalog.addGroup("Z 子组", to: outer)
|
||
let childA = try catalog.addGroup("A 子组", to: outer)
|
||
let leaf = try catalog.addGroup("孙节点", to: childA)
|
||
let tree = CatalogTreeSnapshot.build(
|
||
from: catalog, plugins: NoManagedPluginProvider())
|
||
let rows = tree.visibleRows(source: .manual, collapsed: [])
|
||
|
||
#expect(rows.map(\.id) == [outer, childZ, childA, leaf, root])
|
||
#expect(rows.map(\.depth) == [0, 1, 1, 2, 0])
|
||
#expect(
|
||
tree.visibleRows(
|
||
source: .manual, collapsed: [outer]
|
||
).map(\.id) == [outer, root])
|
||
#expect(tree.roots.map(\.id) == [outer, root])
|
||
#expect(tree.node(withID: outer)?.childNodes.map(\.id) == [childZ, childA])
|
||
}
|
||
|
||
@Test
|
||
func sourceFilteringKeepsDirectoryOrderAndCompleteSubtrees() {
|
||
func node(
|
||
_ id: String, source: NodeSource = .manual, parentID: NodeID? = nil,
|
||
children: [CatalogTreeNode] = []
|
||
) -> CatalogTreeNode {
|
||
CatalogTreeNode(
|
||
id: NodeID(rawValue: id), name: id, isGroup: true, source: source, parentID: parentID,
|
||
createdAt: nil, updatedAt: nil, invalidReasons: [], childCount: children.count,
|
||
subtreeNodeCount: children.count, invalidDescendantCount: 0, children: children)
|
||
}
|
||
let subscription = node(
|
||
"subscription", source: .subscription,
|
||
children: [
|
||
node(
|
||
"subscription-child", source: .subscription,
|
||
parentID: NodeID(rawValue: "subscription"))
|
||
])
|
||
let tree = CatalogTreeSnapshot(roots: [
|
||
node("Z-first"), subscription, node("A-last"),
|
||
])
|
||
#expect(
|
||
tree.visibleRows(source: .manual, collapsed: []).map(\.id.rawValue)
|
||
== ["Z-first", "A-last"])
|
||
let subscriptionRows = tree.visibleRows(source: .subscription, collapsed: [])
|
||
#expect(subscriptionRows.map(\.id.rawValue) == ["subscription", "subscription-child"])
|
||
#expect(subscriptionRows.map(\.depth) == [0, 1])
|
||
#expect(
|
||
tree.visibleRows(source: .subscription, collapsed: [subscription.id]).map(\.id.rawValue)
|
||
== ["subscription"])
|
||
#expect(
|
||
tree.visibleRows(collapsed: []).map(\.id.rawValue)
|
||
== ["Z-first", "subscription", "subscription-child", "A-last"])
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
struct ServerTableInsertionTests {
|
||
@Test
|
||
func insertionLinesUseTheDestinationRowsParent() throws {
|
||
var catalog = ConfigurationCatalog()
|
||
let outer = try catalog.addGroup("外层")
|
||
let inner = try catalog.addGroup("内层", to: outer)
|
||
let leaf = try catalog.addGroup("内层成员", to: inner)
|
||
let sibling = try catalog.addGroup("外层成员", to: outer)
|
||
let root = try catalog.addGroup("根层")
|
||
let tree = CatalogTreeSnapshot.build(
|
||
from: catalog, plugins: NoManagedPluginProvider())
|
||
let rows = tree.visibleRows(collapsed: [])
|
||
#expect(rows.map(\.id) == [outer, inner, leaf, sibling, root])
|
||
|
||
#expect(ServerTableDropState.insertionParent(at: 0, in: rows) == nil)
|
||
#expect(ServerTableDropState.insertionParent(at: 1, in: rows) == outer)
|
||
#expect(ServerTableDropState.insertionParent(at: 2, in: rows) == inner)
|
||
#expect(ServerTableDropState.insertionParent(at: 3, in: rows) == outer)
|
||
#expect(ServerTableDropState.insertionParent(at: 4, in: rows) == nil)
|
||
#expect(ServerTableDropState.insertionParent(at: rows.count, in: rows) == nil)
|
||
|
||
let collapsed = tree.visibleRows(collapsed: [inner])
|
||
#expect(ServerTableDropState.insertionParent(at: 2, in: collapsed) == outer)
|
||
#expect(ServerTableDropState.insertionParent(at: 0, in: []) == nil)
|
||
}
|
||
}
|
||
|
||
/// 活动目标行标记的祖先推导:空心闪电候选是活动目标的严格祖先分组。
|
||
struct CatalogTreeActiveTargetAncestorTests {
|
||
private struct Fixture {
|
||
let tree: CatalogTreeSnapshot
|
||
let outer: NodeID
|
||
let inner: NodeID
|
||
let deepLeaf: NodeID
|
||
let rootServer: NodeID
|
||
}
|
||
|
||
private func nestedTree() throws -> Fixture {
|
||
var catalog = ConfigurationCatalog()
|
||
let outer = try catalog.addGroup("外层")
|
||
let inner = try catalog.addGroup("内层", to: outer)
|
||
let deepLeaf = try catalog.addTestServer("深层服务器", to: inner)
|
||
let rootServer = try catalog.addTestServer("根层服务器")
|
||
let tree = CatalogTreeSnapshot.build(from: catalog, plugins: NoManagedPluginProvider())
|
||
return Fixture(
|
||
tree: tree, outer: outer, inner: inner, deepLeaf: deepLeaf, rootServer: rootServer)
|
||
}
|
||
|
||
@Test
|
||
func absentOrUnknownActiveTargetYieldsNoAncestors() throws {
|
||
let fixture = try nestedTree()
|
||
#expect(fixture.tree.ancestors(of: nil).isEmpty)
|
||
#expect(fixture.tree.ancestors(of: NodeID(rawValue: "nope")).isEmpty)
|
||
}
|
||
|
||
@Test
|
||
func deepServerYieldsEveryAncestorGroup() throws {
|
||
let fixture = try nestedTree()
|
||
#expect(fixture.tree.ancestors(of: fixture.deepLeaf) == [fixture.outer, fixture.inner])
|
||
#expect(fixture.tree.ancestors(of: fixture.rootServer).isEmpty)
|
||
}
|
||
|
||
@Test
|
||
func activeGroupItselfIsExcludedFromItsOwnAncestors() throws {
|
||
let fixture = try nestedTree()
|
||
#expect(fixture.tree.ancestors(of: fixture.inner) == [fixture.outer])
|
||
#expect(fixture.tree.ancestors(of: fixture.outer).isEmpty)
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
struct ServerTableRowDropTests {
|
||
@Test
|
||
func serverRowMovesAnOutsideNodeIntoItsContainingGroup() throws {
|
||
var catalog = ConfigurationCatalog()
|
||
let outer = try catalog.addGroup("外层")
|
||
let inner = try catalog.addGroup("内层", to: outer)
|
||
let member = try catalog.addTestServer("组内服务器", to: inner)
|
||
let outside = try catalog.addTestServer("组外服务器")
|
||
let tree = CatalogTreeSnapshot.build(
|
||
from: catalog, plugins: NoManagedPluginProvider())
|
||
let memberNode = try #require(tree.node(withID: member))
|
||
let parent = ServerTableDropState.rowParent(for: memberNode)
|
||
#expect(parent == inner)
|
||
#expect(ServerTableDropState.rowParent(for: try #require(tree.node(withID: inner))) == inner)
|
||
#expect(ServerTableDropState.rowParent(for: try #require(tree.node(withID: outside))) == nil)
|
||
|
||
try catalog.move(outside, to: parent)
|
||
let movedTree = CatalogTreeSnapshot.build(
|
||
from: catalog, plugins: NoManagedPluginProvider())
|
||
#expect(movedTree.node(withID: outside)?.parentID == inner)
|
||
#expect(movedTree.node(withID: member)?.parentID == inner)
|
||
#expect(movedTree.node(withID: inner)?.childNodes.map(\.id) == [member, outside])
|
||
}
|
||
}
|