Skip to content

Commit b6105ed

Browse files
das7padtimja
andauthored
Fetch exception text separately from console output (#969)
Co-authored-by: Tim Jacomb <[email protected]>
1 parent 61a9019 commit b6105ed

File tree

17 files changed

+262
-56
lines changed

17 files changed

+262
-56
lines changed

src/main/frontend/common/RestClient.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export interface StepLogBufferInfo {
4949
promise: Promise<ConsoleLogData | null>;
5050
};
5151
fullyFetched?: boolean;
52+
exceptionText?: string[];
53+
pendingExceptionText?: Promise<string[]>;
5254
}
5355

5456
// Returned from API, gets converted to 'StepLogBufferInfo'.
@@ -104,6 +106,18 @@ export async function getConsoleTextOffset(
104106
}
105107
}
106108

109+
export async function getExceptionText(stepId: string): Promise<string[]> {
110+
try {
111+
const response = await fetch(`exceptionText?nodeId=${stepId}`);
112+
if (!response.ok) throw response.statusText;
113+
const text = await response.text();
114+
return text.split("\n");
115+
} catch (e) {
116+
console.error(`Caught error when fetching console: '${e}'`);
117+
return [];
118+
}
119+
}
120+
107121
export async function getConsoleBuildOutput(): Promise<string | null> {
108122
try {
109123
const response = await fetch(`consoleBuildOutput`);

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe("ConsoleLogCard", () => {
5252
onMoreConsoleClick: () => {
5353
console.log("onMoreConsoleClick triggered");
5454
},
55+
fetchExceptionText: () => {},
5556
} as ConsoleLogCardProps;
5657

5758
it("renders step header only when not expanded", async () => {

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default function ConsoleLogCard({
2929
isExpanded,
3030
onMoreConsoleClick,
3131
onStepToggle,
32+
fetchExceptionText,
3233
}: ConsoleLogCardProps) {
3334
useEffect(() => {
3435
if (isExpanded) {
@@ -53,15 +54,7 @@ export default function ConsoleLogCard({
5354

5455
const inputStep = step.inputStep;
5556
if (inputStep && !inputStep.parameters) {
56-
return (
57-
<InputStep
58-
step={step}
59-
stepBuffer={stepBuffer}
60-
isExpanded={isExpanded}
61-
onStepToggle={onStepToggle}
62-
onMoreConsoleClick={onMoreConsoleClick}
63-
/>
64-
);
57+
return <InputStep step={step} />;
6558
}
6659

6760
return (
@@ -151,6 +144,7 @@ export default function ConsoleLogCard({
151144
step={step}
152145
stepBuffer={stepBuffer}
153146
onMoreConsoleClick={onMoreConsoleClick}
147+
fetchExceptionText={fetchExceptionText}
154148
isExpanded={false}
155149
onStepToggle={onStepToggle}
156150
/>
@@ -163,6 +157,7 @@ function ConsoleLogBody({
163157
step,
164158
stepBuffer,
165159
onMoreConsoleClick,
160+
fetchExceptionText,
166161
}: ConsoleLogCardProps) {
167162
const prettySizeString = (size: number) => {
168163
const kib = 1024;
@@ -204,6 +199,7 @@ function ConsoleLogBody({
204199
<ConsoleLogStream
205200
logBuffer={stepBuffer}
206201
onMoreConsoleClick={onMoreConsoleClick}
202+
fetchExceptionText={fetchExceptionText}
207203
step={step}
208204
maxHeightScale={0.65}
209205
/>
@@ -218,4 +214,5 @@ export type ConsoleLogCardProps = {
218214
isExpanded: boolean;
219215
onStepToggle: (nodeId: string) => void;
220216
onMoreConsoleClick: (nodeId: string, startByte: number) => void;
217+
fetchExceptionText: (nodeId: string) => void;
221218
};

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.spec.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,26 @@ describe("ConsoleLogStream", () => {
5050
onMoreConsoleClick: () => {
5151
console.log("onMoreConsoleClick triggered");
5252
},
53+
fetchExceptionText: vi.fn(),
5354
} as ConsoleLogStreamProps;
5455

5556
it("renders step console", async () => {
5657
const { findByText } = render(TestComponent({ ...DefaultTestProps }));
5758
expect(findByText(/Hello, world!/));
59+
expect(DefaultTestProps.fetchExceptionText).not.toBeCalled();
60+
});
61+
62+
it("fetches exception text", async () => {
63+
const { findByText } = render(
64+
TestComponent({
65+
...DefaultTestProps,
66+
step: {
67+
...baseStep,
68+
state: Result.failure,
69+
},
70+
}),
71+
);
72+
expect(findByText(/Hello, world!/));
73+
expect(DefaultTestProps.fetchExceptionText).toBeCalledWith(baseStep.id);
5874
});
5975
});

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function ConsoleLogStream({
1616
step,
1717
logBuffer,
1818
onMoreConsoleClick,
19+
fetchExceptionText,
1920
}: ConsoleLogStreamProps) {
2021
const appendInterval = useRef<number | null>(null);
2122
const [stickToBottom, setStickToBottom] = useState(false);
@@ -28,6 +29,12 @@ export default function ConsoleLogStream({
2829
};
2930
}, []);
3031

32+
useEffect(() => {
33+
if (step.state === Result.failure) {
34+
fetchExceptionText(step.id);
35+
}
36+
}, [step.id, step.state, fetchExceptionText]);
37+
3138
useEffect(() => {
3239
if (stickToBottom && logBuffer.lines.length > 0 && canStickToBottom()) {
3340
// Scroll to bottom of the log stream
@@ -125,6 +132,7 @@ export default function ConsoleLogStream({
125132
export interface ConsoleLogStreamProps {
126133
logBuffer: StepLogBufferInfo;
127134
onMoreConsoleClick: (nodeId: string, startByte: number) => void;
135+
fetchExceptionText: (nodeId: string) => void;
128136
step: StepInfo;
129137
maxHeightScale: number;
130138
}

src/main/frontend/pipeline-console-view/pipeline-console/main/NoStageStepsFallback.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function NoStageStepsFallback() {
5252
onMoreConsoleClick={() => {}}
5353
step={step}
5454
maxHeightScale={0.65}
55+
fetchExceptionText={() => {}}
5556
/>
5657
</div>
5758
</div>

src/main/frontend/pipeline-console-view/pipeline-console/main/PipelineConsole.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default function PipelineConsole() {
3636
handleStageSelect,
3737
onStepToggle,
3838
onMoreConsoleClick,
39+
fetchExceptionText,
3940
loading,
4041
} = useStepsPoller({ currentRunPath, previousRunPath });
4142

@@ -137,6 +138,7 @@ export default function PipelineConsole() {
137138
expandedSteps={expandedSteps}
138139
onStepToggle={onStepToggle}
139140
onMoreConsoleClick={onMoreConsoleClick}
141+
fetchExceptionText={fetchExceptionText}
140142
/>
141143
)}
142144
</div>

src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe("StageView", () => {
5151
expandedSteps={["step-1"]}
5252
onStepToggle={vi.fn()}
5353
onMoreConsoleClick={vi.fn()}
54+
fetchExceptionText={vi.fn()}
5455
/>,
5556
);
5657
});

src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function StageView(props: StageViewProps) {
1717
expandedSteps={props.expandedSteps}
1818
onStepToggle={props.onStepToggle}
1919
onMoreConsoleClick={props.onMoreConsoleClick}
20+
fetchExceptionText={props.fetchExceptionText}
2021
/>
2122
</>
2223
);
@@ -29,4 +30,5 @@ export interface StageViewProps {
2930
expandedSteps: string[];
3031
onStepToggle: (nodeId: string) => void;
3132
onMoreConsoleClick: (nodeId: string, startByte: number) => void;
33+
fetchExceptionText: (nodeId: string) => void;
3234
}

src/main/frontend/pipeline-console-view/pipeline-console/main/hooks/use-steps-poller.spec.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,18 @@ vi.mock("../../../../common/tree-api.ts", () => ({
3434
vi.mock("../PipelineConsoleModel.tsx", async () => ({
3535
...(await vi.importActual("../PipelineConsoleModel.tsx")),
3636
getRunSteps: vi.fn(),
37-
getConsoleTextOffset: vi.fn().mockResolvedValue({
38-
text: "log line",
39-
startByte: 0,
40-
endByte: 100,
41-
}),
37+
getConsoleTextOffset: vi.fn(),
38+
getExceptionText: vi.fn().mockResolvedValue(["Error message"]),
4239
POLL_INTERVAL: 50,
4340
}));
4441

4542
beforeEach(() => {
4643
(model.getRunSteps as Mock).mockResolvedValue({ steps: mockSteps });
44+
(model.getConsoleTextOffset as Mock).mockResolvedValue({
45+
text: "log line\n",
46+
startByte: 0,
47+
endByte: 100,
48+
});
4749
window.history.pushState({}, "", "/");
4850
});
4951

@@ -66,6 +68,98 @@ it("selects default step if URL param is missing", async () => {
6668
unmount();
6769
});
6870

71+
it("handles empty console log", async () => {
72+
(model.getConsoleTextOffset as Mock).mockResolvedValue({
73+
text: "",
74+
startByte: 0,
75+
endByte: 0,
76+
});
77+
const { result, unmount } = renderHook(() =>
78+
useStepsPoller({ currentRunPath: "/run/1", previousRunPath: undefined }),
79+
);
80+
81+
await waitFor(() => expect(result.current.expandedSteps).toContain("step-2"));
82+
await waitFor(() =>
83+
expect(result.current.openStageStepBuffers.get("step-2")).to.deep.equal({
84+
startByte: 0,
85+
endByte: 0,
86+
lines: [],
87+
fullyFetched: true,
88+
}),
89+
);
90+
91+
unmount();
92+
});
93+
94+
it("handles empty console log lines before and after text", async () => {
95+
(model.getConsoleTextOffset as Mock).mockResolvedValue({
96+
text: "\nHello\n\n",
97+
startByte: 0,
98+
endByte: 0,
99+
});
100+
const { result, unmount } = renderHook(() =>
101+
useStepsPoller({ currentRunPath: "/run/1", previousRunPath: undefined }),
102+
);
103+
104+
await waitFor(() => expect(result.current.expandedSteps).toContain("step-2"));
105+
await waitFor(() =>
106+
expect(
107+
result.current.openStageStepBuffers.get("step-2")?.lines,
108+
).to.deep.equal(["", "Hello", ""]),
109+
);
110+
111+
unmount();
112+
});
113+
114+
it("appends the exception message", async () => {
115+
const { result, unmount } = renderHook(() =>
116+
useStepsPoller({ currentRunPath: "/run/1", previousRunPath: undefined }),
117+
);
118+
119+
await waitFor(() => expect(result.current.expandedSteps).toContain("step-2"));
120+
await waitFor(() =>
121+
expect(
122+
result.current.openStageStepBuffers.get("step-2")?.lines,
123+
).to.deep.equal(["log line"]),
124+
);
125+
await act(() => result.current.fetchExceptionText("step-2"));
126+
127+
await waitFor(() =>
128+
expect(
129+
result.current.openStageStepBuffers.get("step-2")?.lines,
130+
).to.deep.equal(["log line", "Error message"]),
131+
);
132+
133+
unmount();
134+
});
135+
136+
it("handles empty console log and exception message", async () => {
137+
(model.getConsoleTextOffset as Mock).mockResolvedValue({
138+
text: "",
139+
startByte: 0,
140+
endByte: 0,
141+
});
142+
const { result, unmount } = renderHook(() =>
143+
useStepsPoller({ currentRunPath: "/run/1", previousRunPath: undefined }),
144+
);
145+
146+
await waitFor(() => expect(result.current.expandedSteps).toContain("step-2"));
147+
await waitFor(() =>
148+
expect(
149+
result.current.openStageStepBuffers.get("step-2")?.lines,
150+
).to.deep.equal([]),
151+
);
152+
await act(() => result.current.fetchExceptionText("step-2"));
153+
154+
await waitFor(() =>
155+
expect(
156+
result.current.openStageStepBuffers.get("step-2")?.lines,
157+
).to.deep.equal(["Error message"]),
158+
);
159+
160+
unmount();
161+
});
162+
69163
it("selects the step from URL on initial load", async () => {
70164
window.history.pushState({}, "", "/?selected-node=step-1&start-byte=0");
71165
const { result, unmount } = renderHook(() =>

0 commit comments

Comments
 (0)