From 40b1c4cf416c61b4d4b7645957400bb4718b1026 Mon Sep 17 00:00:00 2001 From: "yongen.loong" Date: Fri, 13 Sep 2024 16:16:09 +0800 Subject: [PATCH] feat: process errors --- components/workspace/format-errors.tsx | 68 +++++++++++++------ .../workspace/process-test-output.test.ts | 45 ++++++++---- components/workspace/process-test-output.ts | 18 ++++- 3 files changed, 96 insertions(+), 35 deletions(-) diff --git a/components/workspace/format-errors.tsx b/components/workspace/format-errors.tsx index c210ba9..4933d5c 100644 --- a/components/workspace/format-errors.tsx +++ b/components/workspace/format-errors.tsx @@ -1,5 +1,6 @@ import { badgeVariants, Badge } from "@/components/ui/badge"; import Link from "next/link"; +import { processTestOutput } from "./process-test-output"; export function FormatErrors({ inputString }: { inputString: string }) { // Detect and remove the dynamic path @@ -13,25 +14,54 @@ export function FormatErrors({ inputString }: { inputString: string }) { /(\w+\/[^\n]*): (error|warning) [^\n]*/g ); - // Print each error or warning message on a new line - if (errorMessages) { - return ( - - - - - - - - - {errorMessages.map((m) => ( - - ))} -
PathTypeDescription
- ); - } else { - return

No errors or warnings.

; - } + const testResults = processTestOutput(inputString); + + return ( + <> + {errorMessages ? ( + + + + + + + + + {errorMessages.map((m) => ( + + ))} +
PathTypeDescription
+ ) : null} + {testResults.length > 0 ? ( + + + + + + + + + {testResults.map((test, key) => ( + + ))} +
StatusNameDuration (ms)
+ ) : null} + + ); +} + +function TestResult({ test }: { test: { status: string; name: string; duration: number, message?: string } }) { + return ( + + + + {test.status} + + + {test.name}
{test?.message} + {test.duration} + + ); } function ErrorMessage({ message }: { message: string }) { diff --git a/components/workspace/process-test-output.test.ts b/components/workspace/process-test-output.test.ts index bbc1b9d..1fe1e34 100644 --- a/components/workspace/process-test-output.test.ts +++ b/components/workspace/process-test-output.test.ts @@ -12,18 +12,18 @@ describe('processTestOutput', () => { it('should return an array of test results for a valid test output string', () => { const result = processTestOutput(testString); expect(result).toEqual([ - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_Empty', duration: 5000 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.HasVoted_ProposalNotFound_ShouldThrow', duration: 2000 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_MultipleUsers_ShouldSucceed', duration: 2000 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_ProposalNotFound_ShouldThrow', duration: 1000 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_Success_Reject', duration: 838 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.CreateProposal_InvalidStartTime_ShouldThrow', duration: 835 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_Success_Approve', duration: 1000 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_ProposalsWithDifferentStates', duration: 715 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Withdraw_ProposalNotFound_ShouldThrow', duration: 1000 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetProposal_Success', duration: 867 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_MultipleProposals', duration: 606 }, - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.InitializeContract_Fail_AlreadyInitialized', duration: 848 } + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_Empty', duration: 5000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.HasVoted_ProposalNotFound_ShouldThrow', duration: 2000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_MultipleUsers_ShouldSucceed', duration: 2000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_ProposalNotFound_ShouldThrow', duration: 1000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_Success_Reject', duration: 838, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.CreateProposal_InvalidStartTime_ShouldThrow', duration: 835, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Vote_Success_Approve', duration: 1000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_ProposalsWithDifferentStates', duration: 715, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.Withdraw_ProposalNotFound_ShouldThrow', duration: 1000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetProposal_Success', duration: 867, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_MultipleProposals', duration: 606, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.InitializeContract_Fail_AlreadyInitialized', duration: 848, message: undefined } ]); }); @@ -37,10 +37,25 @@ describe('processTestOutput', () => { const mixedResultsString = `Test run for /tmp/playground/08993f10-e0ef-4bbf-b6e0-5a5ce4fa71e3/test/bin/Debug/net6.0/MyWayDAO.Tests.dll (.NETCoreApp,Version=v6.0)\nMicrosoft (R) Test Execution Command Line Tool Version 17.3.3 (x64)\nCopyright (c) Microsoft Corporation. All rights reserved.\n\nStarting test execution, please wait...\nA total of 1 test files matched the specified pattern.\n/tmp/playground/08993f10-e0ef-4bbf-b6e0-5a5ce4fa71e3/test/bin/Debug/net6.0/MyWayDAO.Tests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.1 (64-bit .NET 6.0.18)\n[xUnit.net 00:00:03.21] Discovering: MyWayDAO.Tests\n[xUnit.net 00:00:03.27] Discovered: MyWayDAO.Tests\n[xUnit.net 00:00:03.28] Starting: MyWayDAO.Tests\n Passed AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_Empty [5 s]\n Failed AElf.Contracts.MyWayDAO.MyWayDAOTests.HasVoted_ProposalNotFound_ShouldThrow [2 s]\n\nTotal tests: Unknown\n Passed: 1\n Failed: 1\n Total time: 7 Seconds\n\nThe active test run was aborted. Reason: Test host process crashed\nTest Run Aborted with error System.Exception: One or more errors occurred.\n ---> System.Exception: Unable to read beyond the end of the stream.\n at System.IO.BinaryReader.Read7BitEncodedInt()\n at System.IO.BinaryReader.ReadString()\n at Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.NotifyDataAvailable()\n at Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.TcpClientExtensions.MessageLoopAsync(TcpClient client, ICommunicationChannel channel, Action\`1 errorHandler, CancellationToken cancellationToken)\n --- End of inner exception stack trace ---.\n\n`; const result = processTestOutput(mixedResultsString); expect(result).toEqual([ - { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_Empty', duration: 5000 }, - { status: 'failed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.HasVoted_ProposalNotFound_ShouldThrow', duration: 2000 } + { status: 'passed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.GetAllProposals_Empty', duration: 5000, message: undefined }, + { status: 'failed', name: 'AElf.Contracts.MyWayDAO.MyWayDAOTests.HasVoted_ProposalNotFound_ShouldThrow', duration: 2000, message: "" } ]); }); -}); + it('should show the details of the failed test', () => { + const testWithFailure = "Process exited with code 1. Details: Determining projects to restore...\n Restored /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/src/dao.csproj (in 1.28 sec).\n Restored /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/test/dao.Tests.csproj (in 2.66 sec).\nProtobuf/reference/acs12.proto(13,1): warning : warning: Import aelf/core.proto is unused. [/tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/src/dao.csproj]\n dao -> /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/src/bin/Debug/net6.0/dao.dll\n [CONTRACT-PATCHER] Saving as /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/src/bin/Debug/net6.0/dao.dll.patched\nProtobuf/reference/acs12.proto(13,1): warning : warning: Import aelf/core.proto is unused. [/tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/test/dao.Tests.csproj]\n dao.Tests -> /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/test/bin/Debug/net6.0/dao.Tests.dll\nTest run for /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/test/bin/Debug/net6.0/dao.Tests.dll (.NETCoreApp,Version=v6.0)\nMicrosoft (R) Test Execution Command Line Tool Version 17.3.3 (x64)\nCopyright (c) Microsoft Corporation. All rights reserved.\n\nStarting test execution, please wait...\nA total of 1 test files matched the specified pattern.\n/tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/test/bin/Debug/net6.0/dao.Tests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.1 (64-bit .NET 6.0.18)\n[xUnit.net 00:00:02.35] Discovering: dao.Tests\n[xUnit.net 00:00:02.40] Discovered: dao.Tests\n[xUnit.net 00:00:02.40] Starting: dao.Tests\n Passed AElf.Contracts.dao.daoTests.GetAllProposals_ProposalsWithDifferentStates [4 s]\n Passed AElf.Contracts.dao.daoTests.GetAllProposals_MultipleProposals [1 s]\n Passed AElf.Contracts.dao.daoTests.GetProposal_Success [1 s]\n[xUnit.net 00:00:11.85] Shouldly.ShouldAssertException : exception.Message\n[xUnit.net 00:00:11.85] should contain (case insensitive comparison)\n[xUnit.net 00:00:11.85] \"Start time should be greater or equal to current block tie.\"\n[xUnit.net 00:00:11.85] but was actually\n[xUnit.net 00:00:11.85] \"Failed to execute CreateProposal. AElf.Sdk.CSharp.AssertionException: Start time should be greater o...\"\n[xUnit.net 00:00:11.85] Stack Trace:\n[xUnit.net 00:00:11.85] /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/test/daoTests.cs(135,0): at AElf.Contracts.dao.daoTests.CreateProposal_InvalidStartTime_ShouldThrow()\n[xUnit.net 00:00:11.85] at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n[xUnit.net 00:00:11.85] at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n[xUnit.net 00:00:11.85] at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n[xUnit.net 00:00:11.85] --- End of stack trace from previous location ---\n[xUnit.net 00:00:11.85] at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n[xUnit.net 00:00:11.85] at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n[xUnit.net 00:00:11.85] at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n[xUnit.net 00:00:11.85] at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n[xUnit.net 00:00:11.85] at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n[xUnit.net 00:00:11.85] at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n Passed AElf.Contracts.dao.daoTests.InitializeContract_Fail_AlreadyInitialized [1 s]\n Failed AElf.Contracts.dao.daoTests.CreateProposal_InvalidStartTime_ShouldThrow [988 ms]\n Error Message:\n Shouldly.ShouldAssertException : exception.Message\n should contain (case insensitive comparison)\n\"Start time should be greater or equal to current block tie.\"\n but was actually\n\"Failed to execute CreateProposal. AElf.Sdk.CSharp.AssertionException: Start time should be greater o...\"\n Stack Trace:\n at AElf.Contracts.dao.daoTests.CreateProposal_InvalidStartTime_ShouldThrow() in /tmp/playground/362fc693-ea25-4197-bc72-6e7b41255edb/test/daoTests.cs:line 135\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n--- End of stack trace from previous location ---\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n Passed AElf.Contracts.dao.daoTests.CreateProposal_EmptyDescription_ShouldThrow [1 s]\n Passed AElf.Contracts.dao.daoTests.Withdraw_ProposalNotFound_ShouldThrow [939 ms]\n Passed AElf.Contracts.dao.daoTests.Withdraw_MultipleUsersAttempting_Success [977 ms]\n Passed AElf.Contracts.dao.daoTests.GetProposal_ProposalWithVotes_ShouldSucceed [1 s]\n\nTotal tests: Unknown\n Passed: 8\n Failed: 1\n Total time: 21.6233 Seconds\n\n[xUnit.net 00:00:11.85] AElf.Contracts.dao.daoTests.CreateProposal_InvalidStartTime_ShouldThrow [FAIL]\nThe active test run was aborted. Reason: Test host process crashed\nTest Run Aborted with error System.Exception: One or more errors occurred.\n ---> System.Exception: Unable to read beyond the end of the stream.\n at System.IO.BinaryReader.Read7BitEncodedInt()\n at System.IO.BinaryReader.ReadString()\n at Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.NotifyDataAvailable()\n at Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.TcpClientExtensions.MessageLoopAsync(TcpClient client, ICommunicationChannel channel, Action`1 errorHandler, CancellationToken cancellationToken)\n --- End of inner exception stack trace ---.\n\n"; + const result = processTestOutput(testWithFailure); + expect(result).toEqual([ + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.GetAllProposals_ProposalsWithDifferentStates', duration: 4000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.GetAllProposals_MultipleProposals', duration: 1000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.GetProposal_Success', duration: 1000, message: undefined }, + { status: 'failed', name: 'AElf.Contracts.dao.daoTests.CreateProposal_InvalidStartTime_ShouldThrow', duration: 988, message: "Shouldly.ShouldAssertException : exception.Message should contain (case insensitive comparison) \"Start time should be greater or equal to current block tie.\" but was actually \"Failed to execute CreateProposal. AElf.Sdk.CSharp.AssertionException: Start time should be greater o...\"" }, + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.InitializeContract_Fail_AlreadyInitialized', duration: 1000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.CreateProposal_EmptyDescription_ShouldThrow', duration: 1000, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.Withdraw_ProposalNotFound_ShouldThrow', duration: 939, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.Withdraw_MultipleUsersAttempting_Success', duration: 977, message: undefined }, + { status: 'passed', name: 'AElf.Contracts.dao.daoTests.GetProposal_ProposalWithVotes_ShouldSucceed', duration: 1000, message: undefined } + ]); + }); +}); diff --git a/components/workspace/process-test-output.ts b/components/workspace/process-test-output.ts index e2147ee..f6c9cf8 100644 --- a/components/workspace/process-test-output.ts +++ b/components/workspace/process-test-output.ts @@ -3,6 +3,8 @@ export function processTestOutput(str: string) { const results = []; const lines = str.split('\n'); + let lineNum = 0; + for (const line of lines) { const status = line.match(/^\s*(Passed|Failed)\s+/)?.[1].toLowerCase(); @@ -21,8 +23,22 @@ export function processTestOutput(str: string) { duration = parseFloat(durationStr.split(' s')[0]) * 1000; } - results.push({name, status, duration}); + let message: string | undefined; + + if (status === 'failed') { + // Find the error message. It is between the next line and "Stack Trace:" + + const start = lineNum + 2; + const end = lines.slice(start).findIndex(l => l.includes('Stack Trace:')) + start; + message = lines.slice(start, end).map(i => i.trim()).join(' '); + + console.log(message); + } + + results.push({name, status, duration, message}); } + + lineNum++; } return results;