From 898d3629371ecf343e40a5605cbb6c044ae0f5b4 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Sat, 31 Oct 2026 15:21:52 +0800 Subject: [PATCH] Show graph names during right-swipe switching --- .github/workflows/ci.yml | 3 +- app/application.ml | 21 ++++ .../JournalGraphNameAcceptance.swift | 117 ++++++++++++++++++ .../JournalGraphNameStateTests.swift | 53 ++++++++ apple-tests/native-list/README.md | 19 +++ swift/JournalChrome.swift | 67 +++++++++- swift/JournalGraphName.swift | 50 ++++++++ test/application_view_test.ml | 13 ++ tool/test_graph_name.py | 14 +++ 9 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 apple-tests/native-list/JournalGraphNameAcceptance.swift create mode 100644 apple-tests/native-list/JournalGraphNameStateTests.swift create mode 100644 swift/JournalGraphName.swift create mode 100644 tool/test_graph_name.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd54bead..64ea5024 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,11 @@ jobs: - uses: actions/checkout@v4 - name: Verify Apple app icon packaging run: python3 .agents/skills/journal-build/scripts/test_apple_app_icon.py - - name: Verify Swift runtime startup and failure boundaries + - name: Verify Swift runtime and graph feedback boundaries run: | python3 tool/test_journal_runtime_startup.py python3 tool/test_journal_runtime_failure.py + python3 tool/test_graph_name.py - name: Prepare native system libraries before cache lookup run: | brew install pkg-config gmp sqlite zlib zstd diff --git a/app/application.ml b/app/application.ml index 020de3d8..1f0fbc23 100644 --- a/app/application.ml +++ b/app/application.ml @@ -6851,6 +6851,26 @@ let start let owners=Signal.state context.Lui_ui.ui_scheduler [] in let payload=Signal.state context.ui_scheduler "{}" in let refreshers=Hashtbl.create 3 in + (* Resolve the at-most-three resident names only when the catalog or resident + set changes; typing and worker refreshes must not scan the full catalog. *) + let name_catalog = ref None and name_residents = ref [] in + let resident_names = Hashtbl.create 3 in + let target_graph_name state = + match state.manager, next_resident_owner state with + | Some manager, Some owner -> + let same_catalog = match !name_catalog with + | Some previous -> previous == state.graph_catalog | None -> false in + if not same_catalog || !name_residents <> manager.resident_graphs then ( + name_catalog := Some state.graph_catalog; + name_residents := manager.resident_graphs; + Hashtbl.clear resident_names; + Rrbvec.to_seq state.graph_catalog |> Seq.iter (fun (graph:Graph_service.graph) -> + if List.mem graph.graph_id manager.resident_graphs then + Hashtbl.replace resident_names graph.graph_id graph.name)); + (match Hashtbl.find_opt resident_names owner.graph_id with + | Some name -> `String name | None -> `Null) + | _ -> name_catalog := None; name_residents := []; Hashtbl.clear resident_names; `Null + in let root ()=Signal.sample model_signal in let host_model state owner = let model=graph_model state owner in @@ -6883,6 +6903,7 @@ let start ;"pageID",(match selected with Some id->`String id | None->`Null) ;"targetID",(match state.graph_preview with Some id->`String id | None->`Null) ;"targetPageID",(match next_resident_owner state with Some owner->`String(owner_key owner) | None->`Null) + ;"targetGraphName",target_graph_name state ;"residentPageIDs",`List(List.map(fun owner->`String(owner_key owner))live) ;"slotOwners",`List(List.map(fun owner->`String(owner_key owner))next) ;"loadingPageIDs",`List(List.filter_map(fun owner->let model=host_model state owner in diff --git a/apple-tests/native-list/JournalGraphNameAcceptance.swift b/apple-tests/native-list/JournalGraphNameAcceptance.swift new file mode 100644 index 00000000..d1590606 --- /dev/null +++ b/apple-tests/native-list/JournalGraphNameAcceptance.swift @@ -0,0 +1,117 @@ +import XCTest + +/// The native gesture/overlay lifetime is not observable at the OCaml reducer. +/// Run against an isolated three-resident offline warm-start fixture, not an account. +final class JournalGraphNameAcceptance: XCTestCase { + @MainActor private func host(dark: Bool = false, longName: Bool = false, accessibility: Bool = false) -> XCUIApplication { + continueAfterFailure = false + let env = ProcessInfo.processInfo.environment + let app = XCUIApplication(bundleIdentifier: env["JOURNAL_GRAPH_NAME_APP"] ?? "org.logseq.journal.graph-name-probe") + app.launchEnvironment = ["LOGSEQ_JOURNAL_E2EE_TEST_PRIVATE_KEY_STORAGE": "memory", + "LOGSEQ_JOURNAL_E2EE_TEST_WRAPPED_KEY_STORAGE": "memory"] + app.launchArguments = ["--fixture", longName ? "long.json" : "valid.json", "--support-root", longName ? "support-long" : "support-valid", "--application-only", + dark ? "--dark-appearance" : "--light-appearance"] + if accessibility { app.launchArguments.append("--accessibility-size") } + app.launch() + XCTAssertTrue(app.collectionViews.firstMatch.waitForExistence(timeout: 30)) + // Restore the start through public gestures; last-graph persistence and + // virtualization of picker rows must not make test ordering significant. + for _ in 0..<3 { + if title(1, app).exists && title(1, app).isHittable { break } + swipe(app) + } + XCTAssertTrue(title(1, app).waitForExistence(timeout: 20)) + gone(app) + return app + } + @MainActor private func title(_ graph: Int, _ app: XCUIApplication) -> XCUIElement { + app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "Graph \(graph) —")).firstMatch + } + @MainActor private func capsule(_ app: XCUIApplication) -> XCUIElement { + app.staticTexts["graph-switch-name"] + } + @MainActor private func swipe(_ app: XCUIApplication, short: Bool = false) { + // The list's top blank region avoids rows and the existing floating controls. + app.coordinate(withNormalizedOffset: CGVector(dx: 0.15, dy: 0.15)) + .press(forDuration: 0.05, thenDragTo: app.coordinate(withNormalizedOffset: + CGVector(dx: short ? 0.29 : 0.78, dy: 0.15)), withVelocity: .slow, thenHoldForDuration: 0) + } + @MainActor private func record(_ name: String, _ app: XCUIApplication) { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = name; screenshot.lifetime = .keepAlways; add(screenshot) + let tree = XCTAttachment(string: app.debugDescription) + tree.name = name + " accessibility"; tree.lifetime = .keepAlways; add(tree) + } + @MainActor private func gone(_ app: XCUIApplication) { + let absent = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: capsule(app)) + XCTAssertEqual(XCTWaiter.wait(for: [absent], timeout: 2), .completed) + } + @MainActor func testNameCommitAndFade() { + let app = host() + XCTAssertTrue(title(1, app).waitForExistence(timeout: 20)) + XCTAssertFalse(capsule(app).exists) + swipe(app) + XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5), "A right swipe must identify its target") + XCTAssertEqual(capsule(app).label, "Native encrypted graph 2") + XCTAssertTrue(title(2, app).waitForExistence(timeout: 5)) + record("Graph 2 name after real native commit", app) + gone(app) + record("Name fades after commit", app) + } + @MainActor func testCancellationAndContinuousCycle() { + let app = host() + XCTAssertTrue(title(1, app).waitForExistence(timeout: 20)) + swipe(app, short: true) + gone(app) + XCTAssertTrue(title(1, app).isHittable) + for graph in [2, 3, 1] { + swipe(app) + XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5)) + XCTAssertEqual(capsule(app).label, "Native encrypted graph \(graph)") + XCTAssertTrue(title(graph, app).waitForExistence(timeout: 5)) + record("Consecutive graph \(graph)", app) + } + gone(app) + } + @MainActor func testDarkAppearanceAndTouchThrough() { + let app = host(dark: true) + XCTAssertTrue(title(1, app).waitForExistence(timeout: 20)) + swipe(app) + XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5)) + XCTAssertEqual(capsule(app).label, "Native encrypted graph 2") + record("Dark capsule", app) + // The overlay must not consume touches destined for the production page. + app.buttons["Options"].firstMatch.tap() + XCTAssertTrue(app.buttons["Switch graph"].waitForExistence(timeout: 3)) + record("Menu works while name is visible", app) + } + @MainActor func testLongNameStaysClearOfMenu() { + let app = host(longName: true) + XCTAssertTrue(title(1, app).waitForExistence(timeout: 20)) + swipe(app) + XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5)) + XCTAssertEqual(capsule(app).label, "Reading & Research — 工作笔记 and long-term notes") + XCTAssertLessThan(capsule(app).frame.maxX, app.buttons["Options"].firstMatch.frame.minX - 4) + XCTAssertGreaterThan(capsule(app).frame.minX, app.frame.minX + 16) + let progress = app.activityIndicators.firstMatch + if progress.exists { + XCTAssertLessThanOrEqual(capsule(app).frame.maxX + 16, progress.frame.minX) + } + record("Long name preserves full accessibility text", app) + gone(app) + } + + @MainActor func testAccessibilitySizePreservesDateClearance() { + let app = host(accessibility: true) + swipe(app) + XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5)) + // Native List's date accessibility frame includes top padding rather than + // tight glyph bounds. Verify the HUD stays in the existing control band; + // the retained screenshot supplies the date-ink clearance check. + let controls = app.buttons["Options"].firstMatch.frame + XCTAssertGreaterThanOrEqual(capsule(app).frame.minY, controls.minY) + XCTAssertLessThanOrEqual(capsule(app).frame.maxY, controls.maxY) + record("Accessibility size keeps date clear", app) + } + +} diff --git a/apple-tests/native-list/JournalGraphNameStateTests.swift b/apple-tests/native-list/JournalGraphNameStateTests.swift new file mode 100644 index 00000000..dcfca432 --- /dev/null +++ b/apple-tests/native-list/JournalGraphNameStateTests.swift @@ -0,0 +1,53 @@ +import Foundation + +@main struct JournalGraphNameStateTests { + static func main() { + var state = JournalGraphNameFeedback() + state.show(source: "account:A:1", target: "account:B:2", name: "Work — 工作笔记") + let preview = state.presentation! + precondition(preview.name == "Work — 工作笔记" && preview.phase == .preview) + state.focusChanged("account:B:2") + precondition(state.presentation == nil, "An uncommitted external focus is not a successful swipe") + state.show(source: "A", target: "B", name: "Work") + state.commit(source: "A", target: "C") + precondition(state.presentation?.phase == .preview, "Wrong target cannot confirm a request") + state.commit(source: "A", target: "B") + precondition(state.presentation?.phase == .waiting) + state.focusChanged("A") + precondition(state.presentation?.phase == .waiting, "Save may defer focus without a timeout") + let waitingID = state.presentation!.id + state.focusChanged("B") + let confirmed = state.presentation! + precondition(confirmed.phase == .confirmed && confirmed.id != waitingID) + state.show(source: "B", target: "C", name: "Reading") + state.dismiss(confirmed.id) + precondition(state.presentation?.name == "Reading", "An earlier fade must not hide the next swipe") + state.cancel() + state.focusChanged("C") + precondition(state.presentation == nil, "Cancellation rejects a late focus") + state.show(source: "A", target: "B", name: "Work") + state.commit(source: "A", target: "B") + state.focusChanged("C") + precondition(state.presentation == nil, "External selection clears a pending target") + state.show(source: "A:1", target: "B:1", name: "Work") + state.retain(["A:1", "B:2"]) + precondition(state.presentation == nil, "Retired graph generations cannot retain a name") + for name: String? in [nil, "", " \n "] { + state.show(source: "A", target: "B", name: name) + precondition(state.presentation == nil, "Missing names must never reveal graph IDs") + } + state.show(source: "A", target: "B", name: "Work") + state.updateName(target: "C", name: "Other") + precondition(state.presentation?.name == "Work") + state.updateName(target: "B", name: "Renamed work") + precondition(state.presentation?.name == "Renamed work") + state.commit(source: "A", target: "B") + state.focusChanged("B") + state.dismiss(state.presentation!.id) + precondition(state.presentation == nil) + state.show(source: "A", target: "B", name: "Work") + state.retain([]) + precondition(state.presentation == nil, "Account exit clears a visible name") + print("PASS graph-name feedback lifecycle") + } +} diff --git a/apple-tests/native-list/README.md b/apple-tests/native-list/README.md index 934d955b..9f77034c 100644 --- a/apple-tests/native-list/README.md +++ b/apple-tests/native-list/README.md @@ -110,3 +110,22 @@ SwiftUI `ScrollPosition` did not restore this List in the same failing case, so restoration reuses the existing resolved `UIScrollView` and native offset API. `JournalListSnapshotTests.swift` additionally verifies runtime separation, resident-generation retirement and rejection of late offset writes after logout. + +## Graph name feedback + +`JournalGraphNameAcceptance.swift` drives the actual native right-swipe capsule +through an isolated three-resident offline warm-start host. Supply its bundle ID +via `JOURNAL_GRAPH_NAME_APP` (default `org.logseq.journal.graph-name-probe`). +Install generated `valid.json` and `support-valid` in that bundle's Documents; +for the long-name case use `long.json` and an independent `support-long` with +catalog graph 2 named `Reading & Research — 工作笔记 and long-term notes`. +The tests cover target identity, commit/fade, cancellation, a full resident +cycle, native dark appearance, long-name bounds and touches reaching Options. +Transient timing and smooth page motion also require local screen-recording +inspection; XCTest idle waits alone are not a timing/performance measurement. + +Run `python3 tool/test_graph_name.py` for deterministic native feedback ownership: +delayed focus, cancellation, stale fade IDs, external selection, catalog names +and retired graph/account scopes. This small native presentation owner is +outside the OCaml reducer; catalog-to-native name transport is covered once at +the real Application renderer in `application_view_test.ml`. diff --git a/swift/JournalChrome.swift b/swift/JournalChrome.swift index 13b02c3c..ccfcb60d 100644 --- a/swift/JournalChrome.swift +++ b/swift/JournalChrome.swift @@ -240,6 +240,7 @@ struct JournalSheetDismissButton: SwiftUI.View { let pageID: String? let targetID: String? let targetPageID: String? + let targetGraphName: String? let residentPageIDs: [String]? let slotOwners: [String]? let loadingPageIDs: [String]? @@ -295,6 +296,9 @@ struct JournalSheetDismissButton: SwiftUI.View { @State private var committing=false @State private var source:String? @State private var target:String? + @State private var graphName = JournalGraphNameFeedback() + @Environment(\.journalContainerTopInset) private var containerTopInset + @Environment(\.accessibilityReduceTransparency) private var reduceTransparency @StateObject private var handoff=JournalGraphHandoff() @StateObject private var coldMotion=JournalGraphColdMotion() @Environment(\.scenePhase) private var scenePhase @@ -471,13 +475,16 @@ struct JournalSheetDismissButton: SwiftUI.View { guard let data=try? JSONEncoder().encode(value) else {return} JournalExtensions.emit(context:context,payload:data) } - private func reset(notify:Bool,preserveCold:Bool=false) { + private func reset(notify:Bool,preserveCold:Bool=false,preserveName:Bool=false) { if notify,source==properties.pageID {emit("cancel",source:source,target:target)} handoff.cancel() if !preserveCold {coldMotion.cancel()} withTransaction(Transaction(animation:nil)) { translation=0;dragging=false;settling=false;committing=false;source=nil;target=nil } + if !preserveName { + withAnimation(reduceMotion ? nil : .easeOut(duration:0.16)) { graphName.cancel() } + } } private func acknowledge() { if properties.visible==true,coldMotion.ticket?.target != properties.pageID, @@ -490,6 +497,9 @@ struct JournalSheetDismissButton: SwiftUI.View { let from=properties.pageID,let to=properties.targetPageID, from != to,(properties.residentPageIDs ?? []).contains(to) else {return} _=handoff.startGesture();source=from;target=to;dragging=true + withAnimation(reduceMotion ? nil : .easeOut(duration:0.12)) { + graphName.show(source:from,target:to,name:properties.targetGraphName) + } let cold = !slots.contains(where:{$0.owner==to}) && coldMotion.begin(source:from,target:to) if !cold {emit("prepare",source:from,target:to)} translation=min(width,max(0,pan.translation(in:pan.view).x)) @@ -504,6 +514,7 @@ struct JournalSheetDismissButton: SwiftUI.View { let distance=pan.translation(in:pan.view).x,velocity=pan.velocity(in:pan.view).x let commit=distance>width*0.3 || (distance>30 && velocity>650) settling=true;committing=commit + if commit,let source,let target { graphName.commit(source:source,target:target) } if coldMotion.ticket != nil { if commit { withTransaction(Transaction(animation:nil)){translation=width} @@ -558,6 +569,16 @@ struct JournalSheetDismissButton: SwiftUI.View { .background(Color(uiColor:.systemBackground)) .clipped() .overlay{JournalGraphColdSurface(owner:coldMotion).allowsHitTesting(false).accessibilityHidden(true)} + .overlay(alignment:.top) { + if let name = graphName.presentation?.name { + nameCapsule(name) + // Reserve the full 44pt menu + progress + spacing on either side. + .frame(maxWidth:min(240,max(0,bounds.size.width-192))) + .padding(.top,containerTopInset+4) + .transition(.opacity) + .allowsHitTesting(false) + } + } .gesture(RightGraphPan(enabled:properties.enabled==true && !settling && coldMotion.ticket==nil) {handle($0,width:bounds.size.width)}) .onChange(of:bounds.size) { _,_ in reset(notify:true) } } @@ -568,24 +589,32 @@ struct JournalSheetDismissButton: SwiftUI.View { .onChange(of:properties.slotOwners) { _,_ in acknowledge() } .onChange(of:coldMotion.ticket?.id) { _,id in if id==nil {acknowledge()} } .onChange(of:properties.residentPageIDs) { _,ids in + graphName.retain(ids ?? []) coldMotion.retain(ids ?? []) graphScrollOffsets?.retain(ids ?? []) if let source,let target,!(ids ?? []).contains(source) || !(ids ?? []).contains(target) {reset(notify:false)} } .onChange(of:properties.pageID) { _,page in + graphName.focusChanged(page) + let keepName = graphName.presentation?.phase == .confirmed let keepCold=committing && coldMotion.ticket?.target==page if let request=handoff.request,page==request.target { _=handoff.finish(request.id,page:request.target,current:page) } else {handoff.focusChanged(page)} - reset(notify:false,preserveCold:keepCold) + reset(notify:false,preserveCold:keepCold,preserveName:keepName) if keepCold {coldMotion.focused(page)} acknowledge() } + .onChange(of:properties.targetGraphName) { _,name in + graphName.updateName(target:properties.targetPageID,name:name) + } .onChange(of:properties.targetPageID) { _,next in if source==properties.pageID,let target,target != next {reset(notify:true)} } .onChange(of:properties.enabled) { _,enabled in - if enabled != true && handoff.request == nil && coldMotion.ticket?.target != properties.pageID {reset(notify:true)} + if enabled != true && handoff.request == nil && coldMotion.ticket?.target != properties.pageID { + reset(notify:true,preserveName:graphName.presentation?.phase == .confirmed) + } } .onChange(of:properties.targetID) { _,prepared in if prepared == nil && source==properties.pageID && (dragging || settling) {reset(notify:false)} @@ -595,6 +624,38 @@ struct JournalSheetDismissButton: SwiftUI.View { } .onChange(of:scenePhase) { _,phase in if phase != .active {reset(notify:true)} } .onDisappear {reset(notify:false)} + .task(id:graphName.presentation?.id) { + guard let name = graphName.presentation,name.phase == .confirmed else { return } + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification:.announcement,argument:name.name) + } + do { try await Task.sleep(for:.milliseconds(1100)) } catch { return } + withAnimation(reduceMotion ? nil : .easeOut(duration:0.2)) { graphName.dismiss(name.id) } + } + } + + private func nameLabel(_ name:String,font:Font) -> some SwiftUI.View { + HStack(spacing:6) { + Image(systemName:"square.stack").font(.system(size:12)).accessibilityHidden(true) + Text(name).font(font).lineLimit(1).truncationMode(.tail) + .accessibilityIdentifier("graph-switch-name") + } + .padding(.horizontal,12).padding(.vertical,5) + .fixedSize(horizontal:false,vertical:true) + } + + @ViewBuilder private func nameCapsule(_ name:String) -> some SwiftUI.View { + // This transient HUD shares the fixed gap above the date. Native fitting + // preserves that slot at accessibility sizes; VoiceOver retains the full name. + let label = ViewThatFits(in:.vertical) { + nameLabel(name,font:.subheadline.weight(.semibold)) + nameLabel(name,font:.system(size:17,weight:.semibold)) + }.frame(height:32) + if reduceTransparency { + label.background(Color(uiColor:.secondarySystemBackground),in:Capsule()) + } else { + label.glassEffect(.regular,in:.capsule) + } } } diff --git a/swift/JournalGraphName.swift b/swift/JournalGraphName.swift new file mode 100644 index 00000000..2ffd28d0 --- /dev/null +++ b/swift/JournalGraphName.swift @@ -0,0 +1,50 @@ +import Foundation + +/// Transient native feedback follows the gesture's scoped owners, not its name. +struct JournalGraphNameFeedback { + enum Phase { case preview, waiting, confirmed } + struct Presentation: Identifiable { + var id = UUID() + let source: String + let target: String + var name: String + var phase = Phase.preview + } + private(set) var presentation: Presentation? + + mutating func show(source: String, target: String, name: String?) { + presentation = name.flatMap { name in + name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil + : Presentation(source: source, target: target, name: name) + } + } + mutating func commit(source: String, target: String) { + guard presentation?.source == source, presentation?.target == target else { return } + presentation?.phase = .waiting + } + mutating func focusChanged(_ page: String?) { + guard var current = presentation else { return } + if current.phase == .waiting, page == current.target { + current.id = UUID() + current.phase = .confirmed + presentation = current + } else if page != current.source || current.phase != .waiting { + presentation = nil + } + } + mutating func updateName(target: String?, name: String?) { + guard presentation?.target == target else { return } + guard let name, !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + cancel(); return + } + presentation?.name = name + } + mutating func retain(_ pages: [String]) { + if let current = presentation, + !pages.contains(current.source) || !pages.contains(current.target) { cancel() } + } + mutating func dismiss(_ id: UUID) { + if presentation?.id == id { cancel() } + } + mutating func cancel() { presentation = nil } +} diff --git a/test/application_view_test.ml b/test/application_view_test.ml index 24c3d98d..9f03432e 100644 --- a/test/application_view_test.ml +++ b/test/application_view_test.ml @@ -6577,6 +6577,19 @@ let test_graph_picker_controls scenario () = | Some(`String payload)->(try let open Yojson.Safe.Util in let json=Yojson.Safe.from_string payload in member "mode" json=`String "graph-pages" && member "enabled" json=`Bool true with _->false) | _->false)); + if scenario = `Current then ( + (* Graph names are owned by the catalog and transported by the native + renderer, outside the pure route reducer's state/effects. *) + let page = Hashtbl.to_seq_values props |> Seq.find_map (fun fields -> + match List.assoc_opt "payload" fields with + | Some (`String payload) -> + (try let json = Yojson.Safe.from_string payload in + if Yojson.Safe.Util.member "mode" json = `String "graph-pages" + then Some json else None with _ -> None) + | _ -> None) |> Option.get in + Alcotest.(check (option string)) "swipe target carries its full catalog name" + (Some long_name) + (Yojson.Safe.Util.member "targetGraphName" page |> Yojson.Safe.Util.to_string_option)); if scenario = `Late_graph_cancel then ( wait "Favorites action mounts" (fun () -> Option.is_some (find "accessibility-label" "Favorites")); press (Option.get (find "accessibility-label" "Favorites")); diff --git a/tool/test_graph_name.py b/tool/test_graph_name.py new file mode 100644 index 00000000..177b14b9 --- /dev/null +++ b/tool/test_graph_name.py @@ -0,0 +1,14 @@ +"""Verify the native graph-name feedback owner without a runtime or device.""" +from pathlib import Path +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[1] +with tempfile.TemporaryDirectory(prefix='journal-graph-name-') as directory: + executable = Path(directory) / 'graph-name-tests' + subprocess.run(['xcrun', 'swiftc', '-swift-version', '6', '-parse-as-library', + '-module-cache-path', str(Path(directory) / 'modules'), + str(root / 'swift/JournalGraphName.swift'), + str(root / 'apple-tests/native-list/JournalGraphNameStateTests.swift'), + '-o', str(executable)], check=True) + subprocess.run([str(executable)], check=True, timeout=30)