Merge pull request #1724 from manaflow-ai/feat-display-harness-manifest-env
Fix display harness manifest handoff in UI test
This commit is contained in:
commit
63e65a7f5c
9 changed files with 112 additions and 153 deletions
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
|
|
@ -465,54 +465,19 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
HARNESS_DIR="${RUNNER_TEMP}/cmux-display-churn-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
mkdir -p "$HARNESS_DIR"
|
||||
PREFIX="${HARNESS_DIR}/cmux-display-churn"
|
||||
READY_PATH="${PREFIX}.ready"
|
||||
DISPLAY_ID_PATH="${PREFIX}.id"
|
||||
START_PATH="${PREFIX}.start"
|
||||
DONE_PATH="${PREFIX}.done"
|
||||
LOG_PATH="${PREFIX}.log"
|
||||
MANIFEST_PATH="${HARNESS_DIR}/cmux-ui-test-display-harness.json"
|
||||
HELPER_PATH="/tmp/create-virtual-display"
|
||||
MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json"
|
||||
|
||||
rm -f "$READY_PATH" "$DISPLAY_ID_PATH" "$START_PATH" "$DONE_PATH" "$LOG_PATH" "$MANIFEST_PATH"
|
||||
rm -f "$MANIFEST_PATH"
|
||||
trap 'rm -f "$MANIFEST_PATH"' EXIT
|
||||
|
||||
clang -framework Foundation -framework CoreGraphics \
|
||||
-o /tmp/create-virtual-display scripts/create-virtual-display.m
|
||||
|
||||
/tmp/create-virtual-display \
|
||||
--modes 1920x1080,1728x1117,1600x900,1440x810 \
|
||||
--ready-path "$READY_PATH" \
|
||||
--display-id-path "$DISPLAY_ID_PATH" \
|
||||
--start-path "$START_PATH" \
|
||||
--done-path "$DONE_PATH" \
|
||||
--iterations 40 \
|
||||
--interval-ms 40 \
|
||||
>"$LOG_PATH" 2>&1 &
|
||||
VDISPLAY_PID=$!
|
||||
trap 'kill "$VDISPLAY_PID" >/dev/null 2>&1 || true; rm -f "$MANIFEST_PATH"' EXIT
|
||||
|
||||
for _ in {1..120}; do
|
||||
[ -f "$READY_PATH" ] && break
|
||||
sleep 0.25
|
||||
done
|
||||
[ -f "$READY_PATH" ] || {
|
||||
echo "Display harness failed to start" >&2
|
||||
cat "$LOG_PATH" >&2 || true
|
||||
exit 1
|
||||
}
|
||||
-o "$HELPER_PATH" scripts/create-virtual-display.m
|
||||
|
||||
cat >"$MANIFEST_PATH" <<EOF
|
||||
{"readyPath":"$READY_PATH","displayIDPath":"$DISPLAY_ID_PATH","startPath":"$START_PATH","donePath":"$DONE_PATH","logPath":"$LOG_PATH"}
|
||||
{"helperBinaryPath":"$HELPER_PATH"}
|
||||
EOF
|
||||
echo "Display harness manifest: $MANIFEST_PATH"
|
||||
cat "$MANIFEST_PATH"
|
||||
|
||||
CMUX_UI_TEST_DISPLAY_READY_PATH="$READY_PATH" \
|
||||
CMUX_UI_TEST_DISPLAY_ID_PATH="$DISPLAY_ID_PATH" \
|
||||
CMUX_UI_TEST_DISPLAY_START_PATH="$START_PATH" \
|
||||
CMUX_UI_TEST_DISPLAY_DONE_PATH="$DONE_PATH" \
|
||||
CMUX_UI_TEST_DISPLAY_LOG_PATH="$LOG_PATH" \
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
|
|
|
|||
52
.github/workflows/test-e2e.yml
vendored
52
.github/workflows/test-e2e.yml
vendored
|
|
@ -208,56 +208,18 @@ jobs:
|
|||
DISPLAY_ENV_PREFIX=()
|
||||
|
||||
if [ "$TEST_FILTER" = "DisplayResolutionRegressionUITests" ]; then
|
||||
HARNESS_DIR="${RUNNER_TEMP}/cmux-display-churn-${{ github.run_id }}-${{ github.run_attempt }}"
|
||||
mkdir -p "$HARNESS_DIR"
|
||||
PREFIX="${HARNESS_DIR}/cmux-display-churn"
|
||||
READY_PATH="${PREFIX}.ready"
|
||||
DISPLAY_ID_PATH="${PREFIX}.id"
|
||||
START_PATH="${PREFIX}.start"
|
||||
DONE_PATH="${PREFIX}.done"
|
||||
LOG_PATH="${PREFIX}.log"
|
||||
MANIFEST_PATH="${HARNESS_DIR}/cmux-ui-test-display-harness.json"
|
||||
HELPER_PATH="/tmp/create-virtual-display"
|
||||
MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json"
|
||||
|
||||
rm -f "$READY_PATH" "$DISPLAY_ID_PATH" "$START_PATH" "$DONE_PATH" "$LOG_PATH" "$MANIFEST_PATH"
|
||||
rm -f "$MANIFEST_PATH"
|
||||
trap 'rm -f "$MANIFEST_PATH"' EXIT
|
||||
|
||||
clang -framework Foundation -framework CoreGraphics \
|
||||
-o /tmp/create-virtual-display scripts/create-virtual-display.m
|
||||
|
||||
/tmp/create-virtual-display \
|
||||
--modes 1920x1080,1728x1117,1600x900,1440x810 \
|
||||
--ready-path "$READY_PATH" \
|
||||
--display-id-path "$DISPLAY_ID_PATH" \
|
||||
--start-path "$START_PATH" \
|
||||
--done-path "$DONE_PATH" \
|
||||
--iterations 40 \
|
||||
--interval-ms 40 \
|
||||
>"$LOG_PATH" 2>&1 &
|
||||
DISPLAY_VDISPLAY_PID=$!
|
||||
trap 'kill "${DISPLAY_VDISPLAY_PID:-}" >/dev/null 2>&1 || true; rm -f "$MANIFEST_PATH"' EXIT
|
||||
|
||||
for _ in {1..120}; do
|
||||
[ -f "$READY_PATH" ] && break
|
||||
sleep 0.25
|
||||
done
|
||||
[ -f "$READY_PATH" ] || {
|
||||
echo "Display harness failed to start" >&2
|
||||
cat "$LOG_PATH" >&2 || true
|
||||
exit 1
|
||||
}
|
||||
-o "$HELPER_PATH" scripts/create-virtual-display.m
|
||||
|
||||
cat >"$MANIFEST_PATH" <<EOF
|
||||
{"readyPath":"$READY_PATH","displayIDPath":"$DISPLAY_ID_PATH","startPath":"$START_PATH","donePath":"$DONE_PATH","logPath":"$LOG_PATH"}
|
||||
EOF
|
||||
echo "Display harness manifest: $MANIFEST_PATH"
|
||||
cat "$MANIFEST_PATH"
|
||||
|
||||
DISPLAY_ENV_PREFIX=(
|
||||
CMUX_UI_TEST_DISPLAY_READY_PATH="$READY_PATH"
|
||||
CMUX_UI_TEST_DISPLAY_ID_PATH="$DISPLAY_ID_PATH"
|
||||
CMUX_UI_TEST_DISPLAY_START_PATH="$START_PATH"
|
||||
CMUX_UI_TEST_DISPLAY_DONE_PATH="$DONE_PATH"
|
||||
CMUX_UI_TEST_DISPLAY_LOG_PATH="$LOG_PATH"
|
||||
)
|
||||
{"helperBinaryPath":"$HELPER_PATH"}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Start recording right before the test (after build/resolve).
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import XCTest
|
|||
@testable import cmux
|
||||
#endif
|
||||
|
||||
private let lastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut"
|
||||
private let appDelegateLastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut"
|
||||
|
||||
@MainActor
|
||||
final class AppDelegateShortcutRoutingTests: XCTestCase {
|
||||
|
|
@ -725,13 +725,13 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
|
|||
}
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
let originalSetting = defaults.object(forKey: lastSurfaceCloseShortcutDefaultsKey)
|
||||
defaults.set(false, forKey: lastSurfaceCloseShortcutDefaultsKey)
|
||||
let originalSetting = defaults.object(forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey)
|
||||
defaults.set(false, forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey)
|
||||
defer {
|
||||
if let originalSetting {
|
||||
defaults.set(originalSetting, forKey: lastSurfaceCloseShortcutDefaultsKey)
|
||||
defaults.set(originalSetting, forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey)
|
||||
} else {
|
||||
defaults.removeObject(forKey: lastSurfaceCloseShortcutDefaultsKey)
|
||||
defaults.removeObject(forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2737,43 +2737,6 @@ final class CmuxWebViewDragRoutingTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
private struct DragConfigurationOperationsSnapshot: Equatable {
|
||||
let allowCopy: Bool
|
||||
let allowMove: Bool
|
||||
let allowDelete: Bool
|
||||
let allowAlias: Bool
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private enum DragConfigurationSnapshotError: Error {
|
||||
case missingBoolField(primary: String, fallback: String?)
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private func dragConfigurationOperationsSnapshot<T>(from operations: T) throws -> DragConfigurationOperationsSnapshot {
|
||||
let mirror = Mirror(reflecting: operations)
|
||||
|
||||
func readBool(_ primary: String, fallback: String? = nil) throws -> Bool {
|
||||
if let value = mirror.descendant(primary) as? Bool {
|
||||
return value
|
||||
}
|
||||
if let fallback, let value = mirror.descendant(fallback) as? Bool {
|
||||
return value
|
||||
}
|
||||
throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback)
|
||||
}
|
||||
|
||||
return try DragConfigurationOperationsSnapshot(
|
||||
allowCopy: readBool("allowCopy", fallback: "_allowCopy"),
|
||||
allowMove: readBool("allowMove", fallback: "_allowMove"),
|
||||
allowDelete: readBool("allowDelete", fallback: "_allowDelete"),
|
||||
allowAlias: readBool("allowAlias", fallback: "_allowAlias")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
final class BrowserLinkOpenSettingsTests: XCTestCase {
|
||||
private var suiteName: String!
|
||||
private var defaults: UserDefaults!
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import UserNotifications
|
|||
@testable import cmux
|
||||
#endif
|
||||
|
||||
func drainMainQueue() {
|
||||
private func drainBrowserPanelMainQueue() {
|
||||
let expectation = XCTestExpectation(description: "drain main queue")
|
||||
DispatchQueue.main.async {
|
||||
expectation.fulfill()
|
||||
|
|
@ -22,7 +22,7 @@ func drainMainQueue() {
|
|||
}
|
||||
|
||||
@MainActor
|
||||
func makeTemporaryBrowserProfile(named prefix: String) throws -> BrowserProfileDefinition {
|
||||
private func makeTemporaryBrowserPanelProfile(named prefix: String) throws -> BrowserProfileDefinition {
|
||||
try XCTUnwrap(
|
||||
BrowserProfileStore.shared.createProfile(
|
||||
named: "\(prefix)-\(UUID().uuidString)"
|
||||
|
|
@ -107,7 +107,7 @@ final class BrowserPanelOmnibarPillBackgroundColorTests: XCTestCase {
|
|||
@MainActor
|
||||
final class BrowserPanelProfileIsolationTests: XCTestCase {
|
||||
func testStaleDidFinishDoesNotRecordVisitIntoSwitchedProfileHistory() throws {
|
||||
let alternateProfile = try makeTemporaryBrowserProfile(named: "Switched")
|
||||
let alternateProfile = try makeTemporaryBrowserPanelProfile(named: "Switched")
|
||||
let defaultStore = BrowserHistoryStore.shared
|
||||
let alternateStore = BrowserProfileStore.shared.historyStore(for: alternateProfile.id)
|
||||
defaultStore.clearHistory()
|
||||
|
|
@ -137,7 +137,7 @@ final class BrowserPanelProfileIsolationTests: XCTestCase {
|
|||
alternateStore.clearHistory()
|
||||
|
||||
staleDelegate.webView?(staleWebView, didFinish: nil)
|
||||
drainMainQueue()
|
||||
drainBrowserPanelMainQueue()
|
||||
|
||||
XCTAssertTrue(
|
||||
defaultStore.entries.isEmpty,
|
||||
|
|
|
|||
|
|
@ -299,6 +299,42 @@ final class FocusFlashPatternTests: XCTestCase {
|
|||
}
|
||||
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private struct DragConfigurationOperationsSnapshot: Equatable {
|
||||
let allowCopy: Bool
|
||||
let allowMove: Bool
|
||||
let allowDelete: Bool
|
||||
let allowAlias: Bool
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private enum DragConfigurationSnapshotError: Error {
|
||||
case missingBoolField(primary: String, fallback: String?)
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private func dragConfigurationOperationsSnapshot<T>(from operations: T) throws -> DragConfigurationOperationsSnapshot {
|
||||
let mirror = Mirror(reflecting: operations)
|
||||
|
||||
func readBool(_ primary: String, fallback: String? = nil) throws -> Bool {
|
||||
if let value = mirror.descendant(primary) as? Bool {
|
||||
return value
|
||||
}
|
||||
if let fallback, let value = mirror.descendant(fallback) as? Bool {
|
||||
return value
|
||||
}
|
||||
throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback)
|
||||
}
|
||||
|
||||
return try DragConfigurationOperationsSnapshot(
|
||||
allowCopy: readBool("allowCopy", fallback: "_allowCopy"),
|
||||
allowMove: readBool("allowMove", fallback: "_allowMove"),
|
||||
allowDelete: readBool("allowDelete", fallback: "_allowDelete"),
|
||||
allowAlias: readBool("allowAlias", fallback: "_allowAlias")
|
||||
)
|
||||
}
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@MainActor
|
||||
final class InternalTabDragConfigurationTests: XCTestCase {
|
||||
func testDisablesExternalOperationsForInternalTabDrags() throws {
|
||||
|
|
@ -1080,3 +1116,4 @@ final class MarkdownPanelPointerObserverViewTests: XCTestCase {
|
|||
XCTAssertNil(overlay.hitTest(NSPoint(x: 40, y: 30)))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import AppKit
|
|||
@testable import cmux
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
final class WorkspaceManualUnreadTests: XCTestCase {
|
||||
func testShouldClearManualUnreadWhenFocusMovesToDifferentPanel() {
|
||||
let previousFocusedPanelId = UUID()
|
||||
|
|
@ -281,7 +282,7 @@ final class CommandPaletteSwitcherSearchIndexerTests: XCTestCase {
|
|||
XCTAssertFalse(keywords.contains("cmd-palette-indexing"))
|
||||
}
|
||||
|
||||
func testSurfaceDetailOutranksWorkspaceDetailForPathToken() {
|
||||
func testSurfaceDetailOutranksWorkspaceDetailForPathToken() throws {
|
||||
let metadata = CommandPaletteSwitcherSearchMetadata(
|
||||
directories: ["/tmp/worktrees/cmux"],
|
||||
branches: ["feature/cmd-palette"],
|
||||
|
|
|
|||
|
|
@ -1896,4 +1896,3 @@ final class SidebarWorkspaceShortcutHintMetricsTests: XCTestCase {
|
|||
XCTAssertGreaterThan(widened, base)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import XCTest
|
|||
import Foundation
|
||||
|
||||
final class DisplayResolutionRegressionUITests: XCTestCase {
|
||||
private let displayHarnessManifestPath = "/tmp/cmux-ui-test-display-harness.json"
|
||||
private let defaultDisplayHarnessManifestPath = "/tmp/cmux-ui-test-display-harness.json"
|
||||
private var launchTag = ""
|
||||
private var diagnosticsPath = ""
|
||||
private var displayReadyPath = ""
|
||||
|
|
@ -19,14 +19,17 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
|
|||
continueAfterFailure = false
|
||||
|
||||
let token = UUID().uuidString
|
||||
let tempPrefix = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("cmux-ui-test-display-\(token)")
|
||||
.path
|
||||
launchTag = "ui-tests-display-resolution-\(token.prefix(8))"
|
||||
diagnosticsPath = "/tmp/cmux-ui-test-display-churn-\(token).json"
|
||||
displayReadyPath = "/tmp/cmux-ui-test-display-ready-\(token)"
|
||||
displayIDPath = "/tmp/cmux-ui-test-display-id-\(token)"
|
||||
displayStartPath = "/tmp/cmux-ui-test-display-start-\(token)"
|
||||
displayDonePath = "/tmp/cmux-ui-test-display-done-\(token)"
|
||||
helperBinaryPath = "/tmp/cmux-ui-test-display-helper-\(token)"
|
||||
helperLogPath = "/tmp/cmux-ui-test-display-helper-\(token).log"
|
||||
displayReadyPath = "\(tempPrefix).ready"
|
||||
displayIDPath = "\(tempPrefix).id"
|
||||
displayStartPath = "\(tempPrefix).start"
|
||||
displayDonePath = "\(tempPrefix).done"
|
||||
helperBinaryPath = "\(tempPrefix)-helper"
|
||||
helperLogPath = "\(tempPrefix)-helper.log"
|
||||
|
||||
removeTestArtifacts()
|
||||
}
|
||||
|
|
@ -118,11 +121,29 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
|
|||
|
||||
private func prepareDisplayHarnessIfNeeded() throws {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
if let externalHarness = loadExternalHarnessFromEnvironment(env) ?? loadExternalHarnessFromManifest() {
|
||||
displayReadyPath = externalHarness.readyPath
|
||||
displayIDPath = externalHarness.displayIDPath
|
||||
displayStartPath = externalHarness.startPath
|
||||
displayDonePath = externalHarness.donePath
|
||||
if let helperBinaryPath = loadPrebuiltHelperBinaryPath(env) {
|
||||
self.helperBinaryPath = helperBinaryPath
|
||||
try launchDisplayHelper()
|
||||
return
|
||||
}
|
||||
if let externalHarness = loadExternalHarnessFromEnvironment(env) ?? loadExternalHarnessFromManifest(env) {
|
||||
if let helperBinaryPath = externalHarness.helperBinaryPath, !helperBinaryPath.isEmpty {
|
||||
self.helperBinaryPath = helperBinaryPath
|
||||
try launchDisplayHelper()
|
||||
return
|
||||
}
|
||||
guard let readyPath = externalHarness.readyPath, !readyPath.isEmpty,
|
||||
let displayIDPath = externalHarness.displayIDPath, !displayIDPath.isEmpty,
|
||||
let startPath = externalHarness.startPath, !startPath.isEmpty,
|
||||
let donePath = externalHarness.donePath, !donePath.isEmpty else {
|
||||
throw NSError(domain: "DisplayResolutionRegressionUITests", code: 3, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Incomplete external display harness configuration"
|
||||
])
|
||||
}
|
||||
displayReadyPath = readyPath
|
||||
self.displayIDPath = displayIDPath
|
||||
displayStartPath = startPath
|
||||
displayDonePath = donePath
|
||||
if let logPath = externalHarness.logPath, !logPath.isEmpty {
|
||||
helperLogPath = logPath
|
||||
}
|
||||
|
|
@ -133,6 +154,14 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
|
|||
try launchDisplayHelper()
|
||||
}
|
||||
|
||||
private func loadPrebuiltHelperBinaryPath(_ env: [String: String]) -> String? {
|
||||
guard let helperBinaryPath = env["CMUX_UI_TEST_DISPLAY_HELPER_BINARY_PATH"],
|
||||
!helperBinaryPath.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return helperBinaryPath
|
||||
}
|
||||
|
||||
private func loadExternalHarnessFromEnvironment(_ env: [String: String]) -> ExternalDisplayHarness? {
|
||||
guard let readyPath = env["CMUX_UI_TEST_DISPLAY_READY_PATH"], !readyPath.isEmpty,
|
||||
let displayIDPath = env["CMUX_UI_TEST_DISPLAY_ID_PATH"], !displayIDPath.isEmpty,
|
||||
|
|
@ -146,12 +175,14 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
|
|||
displayIDPath: displayIDPath,
|
||||
startPath: startPath,
|
||||
donePath: donePath,
|
||||
logPath: env["CMUX_UI_TEST_DISPLAY_LOG_PATH"]
|
||||
logPath: env["CMUX_UI_TEST_DISPLAY_LOG_PATH"],
|
||||
helperBinaryPath: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func loadExternalHarnessFromManifest() -> ExternalDisplayHarness? {
|
||||
let manifestURL = URL(fileURLWithPath: displayHarnessManifestPath)
|
||||
private func loadExternalHarnessFromManifest(_ env: [String: String]) -> ExternalDisplayHarness? {
|
||||
let manifestPath = env["CMUX_UI_TEST_DISPLAY_HARNESS_MANIFEST_PATH"] ?? defaultDisplayHarnessManifestPath
|
||||
let manifestURL = URL(fileURLWithPath: manifestPath)
|
||||
guard let data = try? Data(contentsOf: manifestURL) else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -377,10 +408,11 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
|
|||
}
|
||||
|
||||
private struct ExternalDisplayHarness: Decodable {
|
||||
let readyPath: String
|
||||
let displayIDPath: String
|
||||
let startPath: String
|
||||
let donePath: String
|
||||
let readyPath: String?
|
||||
let displayIDPath: String?
|
||||
let startPath: String?
|
||||
let donePath: String?
|
||||
let logPath: String?
|
||||
let helperBinaryPath: String?
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue