diff --git a/src/application/ClientRegistrationEnvironment.ts b/src/application/ClientRegistrationEnvironment.ts index ddd11b406d..604bc30993 100644 --- a/src/application/ClientRegistrationEnvironment.ts +++ b/src/application/ClientRegistrationEnvironment.ts @@ -66,14 +66,6 @@ export const clientRegistrationEnvironment = ( validation: "fragment" | "effective" = "effective", platform: NodeJS.Platform = process.platform, ): Result>, ConfigurationError> => { - if (parsed?.dialect === "dsh") { - const environment = dshDocument(parsed.document).environment( - overrides, - validation, - platform, - ); - return environment.ok ? ok(environment.value.values) : environment; - } const details = clientRegistrationEnvironmentDetails( parsed, overrides, diff --git a/src/application/environment/environmentCompare.test.ts b/src/application/environment/environmentCompare.test.ts index 0c189cea3f..7c2676ce4f 100644 --- a/src/application/environment/environmentCompare.test.ts +++ b/src/application/environment/environmentCompare.test.ts @@ -36,7 +36,7 @@ import { parameterScopes, } from "./environmentCompareMatch.js"; import type { EnvironmentManifestTool } from "./environmentSource.js"; -import { isCut, probeRequests } from "./environmentCompareProbe.js"; +import { probeRequests } from "./environmentCompareProbe.js"; import { parseCaptureEvidence } from "./environmentSource.js"; const fixture = environmentStateServeFixture(timedCaptureEvidence(BOOK_ROWS)); @@ -410,12 +410,6 @@ describe("what the comparison translates and learns", () => { ).toBe(0); }); - it("compares a large binary body only by status, as it does large text", () => { - expect(isCut({ kind: "base64", encoded: "AAAA", whole: false })).toBe(true); - expect(isCut({ kind: "base64", encoded: "AAAA", whole: true })).toBe(false); - expect(isCut({ kind: "cut", text: "a" })).toBe(true); - }); - it("translates mapped ids in query values except those that page a list", () => { const call = matchProbeRequest(extraSource.manifest, { method: "GET", diff --git a/src/application/environment/environmentCompareBody.test.ts b/src/application/environment/environmentCompareBody.test.ts index a6e934705f..2535b688ce 100644 --- a/src/application/environment/environmentCompareBody.test.ts +++ b/src/application/environment/environmentCompareBody.test.ts @@ -10,18 +10,20 @@ import { environmentStateServeFixture, } from "../../../tests/fixtures/environmentServe.js"; import { compareEnvironment } from "./environmentCompare.js"; +import { REPLAY_BODY_LIMIT_BYTES } from "./environmentSource.js"; const responseCapture = ( text: string, base64?: string, mimeType = "application/octet-stream", + status = 200, ): ReturnType => { const capture = environmentManifestCaptureFixture([ { startedDateTime: "2026-10-17T18:00:00.000Z", request: { method: "GET", url: "http://app.example/api/blob" }, response: { - status: 200, + status, content: { mimeType, text, @@ -81,6 +83,61 @@ describe("environment comparison response representation", () => { ).toBe(true); }); + it.each([ + { representation: "binary", status: 200 }, + { representation: "binary", status: 503 }, + { representation: "text", status: 200 }, + { representation: "text", status: 503 }, + ] as const)( + "compares a retained $representation body beyond the replay limit by status only ($status)", + ({ representation, status }) => { + const recorded = responseCapture("different recorded body"); + const text = + representation === "binary" + ? Buffer.alloc(REPLAY_BODY_LIMIT_BYTES + 8, 0xff).toString("base64") + : "é".repeat(REPLAY_BODY_LIMIT_BYTES / 2 + 4); + const probe = responseCapture( + text, + representation === "binary" ? text : undefined, + representation === "binary" ? "application/octet-stream" : "text/plain", + status, + ); + const result = compare(recorded, probe); + const equal = status === 200; + expect(result.probes).toMatchObject([ + { + steps: 1, + equal: equal ? 1 : 0, + normalised: { large_bodies: 1, unobserved_bodies: 0 }, + results: [ + { + live_status: status, + env_status: 200, + equal, + categories: equal ? [] : ["status"], + differences: equal + ? [] + : [ + { + category: "status", + pointer: "", + live: "503", + env: "200", + }, + ], + }, + ], + }, + ]); + expect(result.summary).toMatchObject({ + steps: 1, + equal: equal ? 1 : 0, + categories: { status: equal ? 0 : 1, value: 0 }, + normalised: { large_bodies: 1, unobserved_bodies: 0 }, + }); + }, + ); + it("does not conflate a JSON string value with identical plain text", () => { const json = responseCapture('"hello"', undefined, "application/json"); const text = responseCapture("hello"); diff --git a/src/application/environment/environmentCompareProbe.ts b/src/application/environment/environmentCompareProbe.ts index 4b6cc90711..da08ef12d4 100644 --- a/src/application/environment/environmentCompareProbe.ts +++ b/src/application/environment/environmentCompareProbe.ts @@ -93,10 +93,6 @@ export const requestBody = ( : text; }; -/** A replay body the server cut at its size limit. */ -export const isCut = (body: ReplayedBody): boolean => - body.kind === "cut" || (body.kind === "base64" && !body.whole); - /** The application's answer as an environment answer carries it: JSON, text, or null when it is empty; a body the probe did not record is marked. */ const liveAnswer = ( method: string, diff --git a/src/artifacts/inventory/ArtifactInventory.ts b/src/artifacts/inventory/ArtifactInventory.ts index 582c970f1f..26cad6f905 100644 --- a/src/artifacts/inventory/ArtifactInventory.ts +++ b/src/artifacts/inventory/ArtifactInventory.ts @@ -14,18 +14,11 @@ export const inventoryArtifact = async ( inputPath: string, options: ArtifactInventoryOptions, ): Promise => { - const snapshot = await scanArtifactInventory(inputPath, options); - return artifactInventoryResultSchema.parse(snapshot); -}; - -/** Scan an artifact once and retain the complete immutable graph for projection. */ -export const scanArtifactInventory = async ( - inputPath: string, - options: ArtifactInventoryOptions, -): Promise => - options.resourceScope.run(() => + const snapshot = await options.resourceScope.run(() => scanArtifactInventoryInScope(inputPath, options), ); + return artifactInventoryResultSchema.parse(snapshot); +}; export const scanArtifactInventoryInScope = async ( inputPath: string, diff --git a/src/artifacts/inventory/scanCanonical.ts b/src/artifacts/inventory/scanCanonical.ts index ebded85cf3..e905bdcd5d 100644 --- a/src/artifacts/inventory/scanCanonical.ts +++ b/src/artifacts/inventory/scanCanonical.ts @@ -50,15 +50,6 @@ import { type ArtifactInventoryOptions, } from "./types.js"; -export const scanCanonicalArtifactInventory = async ( - path: string, - options: ArtifactInventoryOptions, - readerFactory: typeof createReader = createReader, -): Promise => - options.resourceScope.run(() => - scanCanonicalArtifactInventoryInScope(path, options, readerFactory), - ); - export const scanCanonicalArtifactInventoryInScope = async ( path: string, options: ArtifactInventoryOptions, diff --git a/src/native/parsers/otool.ts b/src/native/parsers/otool.ts index 3028709fae..d3dc96781b 100644 --- a/src/native/parsers/otool.ts +++ b/src/native/parsers/otool.ts @@ -45,7 +45,6 @@ interface ParsedLoadCommands { sdk: string | null; tools: Array<{ name: string; version: string }>; }>; - uuid: string | null; readonly flags: Set; readonly omittedDependencyCommands: string[]; } @@ -158,7 +157,6 @@ export const parseOtoolLoadCommands = (raw: string) => { endian: header.cpuType === null ? null : cpuEndian(header.cpuType), fileType: header.fileType, flags: [...state.flags].sort(), - uuid: state.uuid, commands: state.commands, segments: state.segments, dependencies: state.dependencies, @@ -286,7 +284,6 @@ const createLoadCommandState = ( dependencies: [], entrypoints: [], builds: [], - uuid: null, flags: new Set(headerFlags ?? []), omittedDependencyCommands: [], }); @@ -310,7 +307,6 @@ const parseLoadCommand = (block: string, state: ParsedLoadCommands): void => { kind, fields, }); - collectUuid(kind, fields, state); collectEntrypoint(kind, fields, state); collectBuild(kind, block, fields, state); collectDependency(kind, fields, state); @@ -379,15 +375,6 @@ const scalarFields = (kind: string): readonly string[] => { return common; }; -const collectUuid = ( - kind: string, - fields: Readonly>, - state: ParsedLoadCommands, -): void => { - if (kind === "LC_UUID") - state.uuid = stringField(fields, "uuid") ?? state.uuid; -}; - const collectEntrypoint = ( kind: string, fields: Readonly>, diff --git a/tests/boundary/filesystem/artifactInventorySnapshot.test.ts b/tests/boundary/filesystem/artifactInventorySnapshot.test.ts index a5c470cec3..6802051cf0 100644 --- a/tests/boundary/filesystem/artifactInventorySnapshot.test.ts +++ b/tests/boundary/filesystem/artifactInventorySnapshot.test.ts @@ -15,7 +15,7 @@ import { import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js"; import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js"; -import { scanCanonicalArtifactInventory as scanCanonicalArtifactInventoryOwned } from "../../../src/artifacts/inventory/scanCanonical.js"; +import { scanCanonicalArtifactInventoryInScope } from "../../../src/artifacts/inventory/scanCanonical.js"; import { scanArtifactInventory } from "../../fixtures/artifactInventory.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; @@ -59,11 +59,15 @@ describe("artifact inventory snapshot", () => { }); }, }; - const failure = await scanCanonicalArtifactInventoryOwned( - root, - { resourceScope }, - () => reader, - ).catch((cause: unknown) => cause); + const failure = await resourceScope + .run(() => + scanCanonicalArtifactInventoryInScope( + root, + { resourceScope }, + () => reader, + ), + ) + .catch((cause: unknown) => cause); expect(failure).toBeInstanceOf(ArtifactReaderFailure); if (!(failure instanceof ArtifactReaderFailure)) throw failure instanceof Error @@ -156,11 +160,15 @@ it("unwinds every nested traversal and preserves its primary failure", async () close: () => directory.close(), }; - const failure = await scanCanonicalArtifactInventoryOwned( - bundle, - { resourceScope }, - () => reader, - ).catch((cause: unknown) => cause); + const failure = await resourceScope + .run(() => + scanCanonicalArtifactInventoryInScope( + bundle, + { resourceScope }, + () => reader, + ), + ) + .catch((cause: unknown) => cause); expect(failure).toMatchObject({ reason: "integrity", message: "primary nested member failure", diff --git a/tests/boundary/filesystem/asarInventoryFreshness.test.ts b/tests/boundary/filesystem/asarInventoryFreshness.test.ts index 7174d96473..f1ecc1b310 100644 --- a/tests/boundary/filesystem/asarInventoryFreshness.test.ts +++ b/tests/boundary/filesystem/asarInventoryFreshness.test.ts @@ -10,7 +10,7 @@ import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceSc import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js"; import { inventoryArtifact } from "../../fixtures/artifactInventory.js"; import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js"; -import { scanCanonicalArtifactInventory as scanCanonicalArtifactInventoryOwned } from "../../../src/artifacts/inventory/scanCanonical.js"; +import { scanCanonicalArtifactInventoryInScope } from "../../../src/artifacts/inventory/scanCanonical.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const CONTENT = "module.exports = 42;\n"; @@ -226,27 +226,29 @@ describe("ASAR captured-byte identity", () => { onTestFinished(() => resourceScope.close()); try { await expect( - scanCanonicalArtifactInventoryOwned( - fixture.bundle, - { resourceScope }, - () => ({ - format: directory.format, - entries: (signal) => directory.entries(signal), - provenance: () => directory.provenance(), - close: () => directory.close(), - async open(entry, signal) { - const source = await directory.open(entry, signal); - return Readable.from( - (async function* () { - yield* source; - await fixture.replace(); - expect((await lstat(fixture.archive)).size).toBe( - originalSize, - ); - })(), - ); - }, - }), + resourceScope.run(() => + scanCanonicalArtifactInventoryInScope( + fixture.bundle, + { resourceScope }, + () => ({ + format: directory.format, + entries: (signal) => directory.entries(signal), + provenance: () => directory.provenance(), + close: () => directory.close(), + async open(entry, signal) { + const source = await directory.open(entry, signal); + return Readable.from( + (async function* () { + yield* source; + await fixture.replace(); + expect((await lstat(fixture.archive)).size).toBe( + originalSize, + ); + })(), + ); + }, + }), + ), ), ).rejects.toMatchObject({ reason: "integrity", @@ -310,11 +312,15 @@ describe("ASAR captured-byte identity", () => { const resourceScope = new ArtifactResourceScope(); onTestFinished(() => resourceScope.close()); try { - const failure: unknown = await scanCanonicalArtifactInventoryOwned( - fixture.archive, - { resourceScope }, - () => reader, - ).catch((cause: unknown) => cause); + const failure: unknown = await resourceScope + .run(() => + scanCanonicalArtifactInventoryInScope( + fixture.archive, + { resourceScope }, + () => reader, + ), + ) + .catch((cause: unknown) => cause); expect(failure).toMatchObject({ cleanup: { resources: [attempts[0]] }, partialObservation: { diff --git a/tests/boundary/filesystem/dshClientConfiguration.test.ts b/tests/boundary/filesystem/dshClientConfiguration.test.ts index ec7f2e5cdb..2ab9d38e45 100644 --- a/tests/boundary/filesystem/dshClientConfiguration.test.ts +++ b/tests/boundary/filesystem/dshClientConfiguration.test.ts @@ -1788,8 +1788,9 @@ describe("DeepSeek Harness runtime environment validation", () => { { HOPPER_LOADER_ARGS_JSON: "{" }, command, ), - ).rejects.toMatchObject({ - settings: [{ setting: "HOPPER_LOADER_ARGS_JSON" }], + ).resolves.toMatchObject({ + status: "invalid", + remediation: expect.stringContaining("HOPPER_LOADER_ARGS_JSON"), }); await expect( inspectClientConfiguration( @@ -1797,8 +1798,8 @@ describe("DeepSeek Harness runtime environment validation", () => { { REA_GHIDRA_LANGUAGE_ID: "" }, command, ), - ).rejects.toMatchObject({ - settings: [{ setting: "REA_GHIDRA_LANGUAGE_ID" }], + ).resolves.toMatchObject({ + status: "invalid", }); expect( await inspectClientConfiguration( diff --git a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts index 8ac69daae7..ecc87ff129 100644 --- a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts +++ b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts @@ -13,7 +13,10 @@ import { build } from "plist"; import { describe, expect, it, onTestFinished, vi } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; -import { execFileOutput } from "../../../../src/process/ExecFileOutput.js"; +import { + ExecFileOutputCancelledError, + execFileOutput, +} from "../../../../src/process/ExecFileOutput.js"; import { ArtifactReaderFailure, type ArtifactEntry, @@ -26,7 +29,7 @@ import { NativeDmgArtifactReader, type NativeDmgHost, } from "../../../../src/artifacts/NativeDmgArtifactReader.js"; -import { scanCanonicalArtifactInventory } from "../../../../src/artifacts/inventory/scanCanonical.js"; +import { scanCanonicalArtifactInventoryInScope } from "../../../../src/artifacts/inventory/scanCanonical.js"; const createAttachedReader = async ( path: string, @@ -219,19 +222,23 @@ describe("DMG inventory reader ownership", () => { let closeCalls = 0; const resourceScope = new ArtifactResourceScope(); onTestFinished(() => resourceScope.close()); - const failure = await scanCanonicalArtifactInventory( - path, - { resourceScope }, - (inputPath, _format, environment) => { - reader = new NativeDmgArtifactReader(inputPath, environment, host); - const closeReader = reader.close.bind(reader); - reader.close = () => { - closeCalls += 1; - return closeReader(); - }; - return reader; - }, - ).catch((cause: unknown) => cause); + const failure = await resourceScope + .run(() => + scanCanonicalArtifactInventoryInScope( + path, + { resourceScope }, + (inputPath, _format, environment) => { + reader = new NativeDmgArtifactReader(inputPath, environment, host); + const closeReader = reader.close.bind(reader); + reader.close = () => { + closeCalls += 1; + return closeReader(); + }; + return reader; + }, + ), + ) + .catch((cause: unknown) => cause); expect(failure).toMatchObject({ reason: "format", @@ -938,80 +945,97 @@ describe("DMG attach fallback recovery", () => { }); describe("DMG attach cancellation and preexisting mounts", () => { - it("waits for an aborted attach result before discovering and detaching its mount", async () => { - const calls: string[][] = []; - const controller = new AbortController(); - const mount = createMountFixture(); - let markAttachStarted = (): void => {}; - const attachStarted = new Promise((resolveStarted) => { - markAttachStarted = resolveStarted; - }); - let attached = false; - let mountPoint: string | undefined; - const host: NativeDmgHost = { - async run(arguments_, signal) { - calls.push([...arguments_]); - if (arguments_[0] === "info") - return { - stdout: - !attached || mountPoint === undefined - ? emptyInfoPlist - : infoPlist("/tmp/image.dmg", [ - { - "dev-entry": "/dev/disk21", - "mount-point": mountPoint, - }, - ]), - exitCode: 0, - }; - if (arguments_[0] === "detach") { - attached = false; - return { stdout: "", exitCode: 0 }; - } - if (arguments_[0] !== "attach") return { stdout: "", exitCode: 0 }; - mountPoint = join(mount.rememberRoot(arguments_), "Fixture"); - markAttachStarted(); - return new Promise((_, reject) => { - signal?.addEventListener( - "abort", - () => { - void mkdir(mountPoint ?? "").then(async () => { - attached = true; - await writeFile( - join(mountPoint ?? "", "hello.txt"), - "after exit", - ); - reject( - Object.assign(new Error("attach aborted"), { - name: "AbortError", - }), - ); - }); - }, - { once: true }, - ); - }); - }, - }; + it.each(["owned cancellation", "opaque AbortError"] as const)( + "waits for an aborted attach result before discovering and detaching its mount (%s)", + async (kind) => { + const calls: string[][] = []; + const controller = new AbortController(); + const reason = { request: "cancel DMG attach" }; + const commandFailure = + kind === "owned cancellation" + ? new ExecFileOutputCancelledError(reason) + : Object.assign(new Error("attach aborted"), { name: "AbortError" }); + const mount = createMountFixture(); + let markAttachStarted = (): void => {}; + const attachStarted = new Promise((resolveStarted) => { + markAttachStarted = resolveStarted; + }); + let attached = false; + let mountPoint: string | undefined; + const host: NativeDmgHost = { + async run(arguments_, signal) { + calls.push([...arguments_]); + if (arguments_[0] === "info") + return { + stdout: + !attached || mountPoint === undefined + ? emptyInfoPlist + : infoPlist("/tmp/image.dmg", [ + { + "dev-entry": "/dev/disk21", + "mount-point": mountPoint, + }, + ]), + exitCode: 0, + }; + if (arguments_[0] === "detach") { + attached = false; + return { stdout: "", exitCode: 0 }; + } + if (arguments_[0] !== "attach") return { stdout: "", exitCode: 0 }; + mountPoint = join(mount.rememberRoot(arguments_), "Fixture"); + markAttachStarted(); + return new Promise((_, reject) => { + signal?.addEventListener( + "abort", + () => { + void mkdir(mountPoint ?? "").then(async () => { + attached = true; + await writeFile( + join(mountPoint ?? "", "hello.txt"), + "after exit", + ); + reject(commandFailure); + }); + }, + { once: true }, + ); + }); + }, + }; - const creating = createAttachedReader( - "/tmp/image.dmg", - process.env, - controller.signal, - host, - ); - await attachStarted; - controller.abort(); - await expect(creating).rejects.toMatchObject({ reason: "cancelled" }); - expect(calls.map(([operation]) => operation)).toEqual([ - "verify", - "info", - "attach", - "info", - "detach", - ]); - expect(calls).toContainEqual(["detach", "/dev/disk21"]); - }); + const creating = createAttachedReader( + "/tmp/image.dmg", + process.env, + controller.signal, + host, + ); + await attachStarted; + controller.abort(reason); + const failure: unknown = await creating.catch((cause: unknown) => cause); + if (!(failure instanceof ArtifactReaderFailure)) + throw new Error("Expected the settled attach failure"); + expect(failure.reason).toBe( + kind === "owned cancellation" ? "cancelled" : "unavailable", + ); + expect(failure.cause).toBe(commandFailure); + if (kind === "owned cancellation") + expect(failure.cancellation?.reason).toBe(reason); + else expect(failure.cancellation).toBeUndefined(); + expect(calls.map(([operation]) => operation)).toEqual([ + "verify", + "info", + "attach", + "info", + "detach", + ]); + expect(calls).toContainEqual(["detach", "/dev/disk21"]); + expect(attached).toBe(false); + await expect(realpath(mount.root() ?? "")).rejects.toMatchObject({ + code: "ENOENT", + }); + }, + ); it("leaves a preexisting outside-root mount attached", async () => { const calls: string[][] = []; diff --git a/tests/boundary/providers/native/nativeInventory.golden.test.ts b/tests/boundary/providers/native/nativeInventory.golden.test.ts index 64407bb426..ffc1915e2f 100644 --- a/tests/boundary/providers/native/nativeInventory.golden.test.ts +++ b/tests/boundary/providers/native/nativeInventory.golden.test.ts @@ -663,7 +663,9 @@ Load command 1 ); expect(parsed.commands[0]?.fields.name).toBe(`${installName} (offset 24)`); expect(parsed.dependencies).toEqual([]); - expect(parsed.uuid).toBe("01234567-89AB-CDEF-0123-456789ABCDEF"); + expect(parsed.commands[1]?.fields.uuid).toBe( + "01234567-89AB-CDEF-0123-456789ABCDEF", + ); }, ); it.each(["weak-def", "absolute", "literal suffix"])( diff --git a/tests/fixtures/artifactInventory.ts b/tests/fixtures/artifactInventory.ts index 776d55fbea..4ec030a50b 100644 --- a/tests/fixtures/artifactInventory.ts +++ b/tests/fixtures/artifactInventory.ts @@ -4,9 +4,9 @@ import { ArtifactReaderFailure } from "../../src/artifacts/ArtifactReader.js"; import { classifyAndHashRoot as classifyAndHashRootOwned } from "../../src/artifacts/inventory/classify.js"; import { inventoryArtifact as inventoryArtifactOwned, - scanArtifactInventory as scanArtifactInventoryOwned, + scanArtifactInventoryInScope, } from "../../src/artifacts/inventory/ArtifactInventory.js"; -import { scanCanonicalArtifactInventory as scanCanonicalArtifactInventoryOwned } from "../../src/artifacts/inventory/scanCanonical.js"; +import { scanCanonicalArtifactInventoryInScope } from "../../src/artifacts/inventory/scanCanonical.js"; import type { createReader } from "../../src/artifacts/inventory/reader.js"; import { materializeArtifactInventory as materializeArtifactInventoryOwned, @@ -43,7 +43,9 @@ export const scanArtifactInventory = ( options: InventoryOptions = {}, ) => withArtifactResourceScope((resourceScope) => - scanArtifactInventoryOwned(path, { ...options, resourceScope }), + resourceScope.run(() => + scanArtifactInventoryInScope(path, { ...options, resourceScope }), + ), ); export const scanCanonicalArtifactInventory = ( @@ -52,10 +54,12 @@ export const scanCanonicalArtifactInventory = ( readerFactory?: typeof createReader, ) => withArtifactResourceScope((resourceScope) => - scanCanonicalArtifactInventoryOwned( - path, - { ...options, resourceScope }, - readerFactory, + resourceScope.run(() => + scanCanonicalArtifactInventoryInScope( + path, + { ...options, resourceScope }, + readerFactory, + ), ), );