Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import { ChildProcessSpawner } from "effect/process";
import * as Metric from "effect/Metric";
import { assert, it } from "@effect/vitest";

import { CheckpointRef, GitCommandError, VcsProcessExitError } from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import * as ServerConfig from "../config.ts";
import { gitCommandDuration, gitCommandsTotal } from "../observability/Metrics.ts";
import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
import * as ProcessRunner from "../processRunner.ts";
import * as GitVcsDriver from "./GitVcsDriver.ts";
Expand Down Expand Up @@ -1491,3 +1493,124 @@ it.effect.each(["win32", "linux"] as const)(
}
}).pipe(Effect.scoped, Effect.provide(layerGitContract)),
);

const gitCommandCount = (operation: string, outcome: string) =>
Metric.value(
Metric.withAttributes(gitCommandsTotal, [
["operation", operation],
["outcome", outcome],
]),
).pipe(Effect.map((state) => state.count));

const gitCommandDurationCount = (operation: string) =>
Metric.value(Metric.withAttributes(gitCommandDuration, [["operation", operation]])).pipe(
Effect.map((state) => state.count),
);

const makeMeteredDriver = (exitCode: number, failure?: VcsProcessExitError) =>
GitVcsDriver.makeVcsDriverShape().pipe(
Effect.provideService(VcsProcess.VcsProcess, {
run: () =>
failure === undefined
? Effect.succeed({
exitCode: ChildProcessSpawner.ExitCode(exitCode),
stdout: "",
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
})
: Effect.fail(failure),
}),
);

it.effect("GitVcsDriver meters each git command once under its operation", () =>
Effect.gen(function* () {
const operation = "GitVcsDriver.checkpoints.test.metered";
const driver = yield* makeMeteredDriver(0);

yield* driver.execute({ operation, cwd: "/repo", args: ["status"] });
yield* driver.execute({ operation, cwd: "/repo", args: ["status"] });

assert.strictEqual(yield* gitCommandCount(operation, "success"), 2);
assert.strictEqual(yield* gitCommandCount(operation, "failure"), 0);
assert.strictEqual(yield* gitCommandDurationCount(operation), 2);
}).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("GitVcsDriver counts an allowed non-zero exit as a success", () =>
Effect.gen(function* () {
const operation = "GitVcsDriver.checkpoints.test.allowedNonZero";
const driver = yield* makeMeteredDriver(1);

const result = yield* driver.execute({
operation,
cwd: "/repo",
args: ["diff", "--quiet"],
allowNonZeroExit: true,
});

assert.strictEqual(result.exitCode, 1);
assert.strictEqual(yield* gitCommandCount(operation, "success"), 1);
assert.strictEqual(yield* gitCommandCount(operation, "failure"), 0);
}).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("GitVcsDriver counts a failed git command as a failure", () =>
Effect.gen(function* () {
const operation = "GitVcsDriver.checkpoints.test.failed";
const driver = yield* makeMeteredDriver(
128,
new VcsProcessExitError({
operation,
command: "git",
cwd: "/repo",
exitCode: 128,
detail: "fatal",
}),
);

const error = yield* driver
.execute({ operation, cwd: "/repo", args: ["rev-parse", "HEAD"] })
.pipe(Effect.flip);

assert.strictEqual(error._tag, "VcsProcessExitError");
assert.strictEqual(yield* gitCommandCount(operation, "failure"), 1);
assert.strictEqual(yield* gitCommandCount(operation, "success"), 0);
assert.strictEqual(yield* gitCommandDurationCount(operation), 1);
}).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("checkpoint capture meters every git process it starts, including streamed ones", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const liveProcess = yield* VcsProcess.VcsProcess;
const operation = VcsProcess.CHECKPOINT_CAPTURE_OPERATION;
let started = 0;
let streamed = 0;
const driver = yield* GitVcsDriver.makeVcsDriverShape();
const captureDriver = yield* GitVcsDriver.makeVcsDriverShape().pipe(
Effect.provideService(VcsProcess.VcsProcess, {
run: (input) => {
if (input.operation === operation) {
started++;
if (input.onStdoutChunk !== undefined) streamed++;
}
return liveProcess.run(input);
},
}),
);
const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-checkpoint-metrics-" });
const { checkpointRef } = yield* makeCheckpointFixture(driver, cwd);
const before =
(yield* gitCommandCount(operation, "success")) +
(yield* gitCommandCount(operation, "failure"));

yield* captureDriver.checkpoints.captureCheckpoint({ cwd, checkpointRef });

const after =
(yield* gitCommandCount(operation, "success")) +
(yield* gitCommandCount(operation, "failure"));
assert.isTrue(streamed > 0);
assert.strictEqual(after - before, started);
}).pipe(Effect.scoped, Effect.provide(layerGitContract)),
);
17 changes: 15 additions & 2 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from "@t3tools/contracts";
import { isMissingExecutable } from "@t3tools/source-control-core/server/discovery";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts";
import {
makeGitVcsDriverCore,
PATCH_RENDER_PREFIX_ARGS,
Expand Down Expand Up @@ -503,6 +504,18 @@ function parseGitRemoteVerboseOutput(
return remotes;
}

const runGitProcess = (
process: VcsProcess.VcsProcess["Service"],
input: VcsProcess.VcsProcessInput,
) =>
process.run(input).pipe(
withMetrics({
counter: gitCommandsTotal,
timer: gitCommandDuration,
attributes: { operation: input.operation },
}),
);

const gitCommand = (
process: VcsProcess.VcsProcess["Service"],
operation: string,
Expand All @@ -518,7 +531,7 @@ const gitCommand = (
readonly appendTruncationMarker?: boolean;
},
) =>
process.run({
runGitProcess(process, {
operation,
command: "git",
args: ["-C", cwd, ...args],
Expand Down Expand Up @@ -903,7 +916,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
let skipped = false;
let skippedRecord: number[] = [];
const skippedPaths: string[] = [];
yield* vcsProcess.run({
yield* runGitProcess(vcsProcess, {
operation,
command: "git",
cwd: input.cwd,
Expand Down
Loading