From ed42c6c399a4e9f870d527cb4d5862d12a9b803d Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 23 May 2023 10:33:37 -0400 Subject: [PATCH] make the test case results errors work with WorkflowTaskException and use in ErrorForDisplay --- .../process_model_test_runner_service.py | 51 ++++++++++-- .../src/components/ErrorDisplay.tsx | 17 ++++ .../src/components/ProcessModelTestRun.tsx | 79 +++++++++++++++++-- spiffworkflow-frontend/src/interfaces.ts | 23 ++++++ 4 files changed, 157 insertions(+), 13 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 01057d44..81161548 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -2,12 +2,14 @@ import glob import json import os import re +import traceback from dataclasses import dataclass from typing import Any from typing import Callable from typing import Optional from lxml import etree # type: ignore +from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState @@ -43,12 +45,23 @@ class MissingInputTaskData(Exception): pass +@dataclass +class TestCaseErrorDetails: + error_messages: list[str] + task_error_line: Optional[str] = None + task_trace: Optional[list[str]] = None + task_bpmn_identifier: Optional[str] = None + task_bpmn_name: Optional[str] = None + task_line_number: Optional[int] = None + stacktrace: Optional[list[str]] = None + + @dataclass class TestCaseResult: passed: bool bpmn_file: str test_case_identifier: str - error_messages: Optional[list[str]] = None + test_case_error_details: Optional[TestCaseErrorDetails] = None DEFAULT_NSMAP = { @@ -127,8 +140,8 @@ class ProcessModelTestRunner: formatted_tests = ["FAILING TESTS:"] for failing_test in self.failing_tests(): msg = "" - if failing_test.error_messages: - msg = "\n\t\t".join(failing_test.error_messages) + if failing_test.test_case_error_details is not None: + msg = "\n\t\t".join(failing_test.test_case_error_details.error_messages) formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_identifier}: {msg}") return "\n".join(formatted_tests) @@ -147,8 +160,7 @@ class ProcessModelTestRunner: try: self.run_test_case(bpmn_file, test_case_identifier, test_case_contents) except Exception as ex: - ex_as_array = str(ex).split("\n") - self._add_test_result(False, bpmn_file, test_case_identifier, ex_as_array) + self._add_test_result(False, bpmn_file, test_case_identifier, exception=ex) def run_test_case(self, bpmn_file: str, test_case_identifier: str, test_case_contents: dict) -> None: bpmn_process_instance = self._instantiate_executer(bpmn_file) @@ -329,15 +341,40 @@ class ProcessModelTestRunner: def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) + def _exception_to_test_case_error_details(self, exception: Exception) -> TestCaseErrorDetails: + error_messages = str(exception).split("\n") + test_case_error_details = TestCaseErrorDetails(error_messages=error_messages) + if isinstance(exception, WorkflowTaskException): + test_case_error_details.task_error_line = exception.error_line + test_case_error_details.task_trace = exception.task_trace + test_case_error_details.task_line_number = exception.line_number + test_case_error_details.task_bpmn_identifier = exception.task_spec.bpmn_id + test_case_error_details.task_bpmn_name = exception.task_spec.bpmn_name + else: + test_case_error_details.stacktrace = traceback.format_exc().split("\n") + + return test_case_error_details + def _add_test_result( - self, passed: bool, bpmn_file: str, test_case_identifier: str, error_messages: Optional[list[str]] = None + self, + passed: bool, + bpmn_file: str, + test_case_identifier: str, + error_messages: Optional[list[str]] = None, + exception: Optional[Exception] = None, ) -> None: + test_case_error_details = None + if exception is not None: + test_case_error_details = self._exception_to_test_case_error_details(exception) + elif error_messages: + test_case_error_details = TestCaseErrorDetails(error_messages=error_messages) + bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file) test_result = TestCaseResult( passed=passed, bpmn_file=bpmn_file_relative, test_case_identifier=test_case_identifier, - error_messages=error_messages, + test_case_error_details=test_case_error_details, ) self.test_case_results.append(test_result) diff --git a/spiffworkflow-frontend/src/components/ErrorDisplay.tsx b/spiffworkflow-frontend/src/components/ErrorDisplay.tsx index 67e6e18d..76009418 100644 --- a/spiffworkflow-frontend/src/components/ErrorDisplay.tsx +++ b/spiffworkflow-frontend/src/components/ErrorDisplay.tsx @@ -4,6 +4,7 @@ import { ErrorForDisplay, ProcessInstanceEventErrorDetail, ProcessInstanceLogEntry, + TestCaseErrorDetails, } from '../interfaces'; function errorDetailDisplay( @@ -40,6 +41,22 @@ export const errorForDisplayFromProcessInstanceErrorDetail = ( return errorForDisplay; }; +export const errorForDisplayFromTestCaseErrorDetails = ( + testCaseErrorDetails: TestCaseErrorDetails +) => { + const errorForDisplay: ErrorForDisplay = { + message: testCaseErrorDetails.error_messages.join('\n'), + messageClassName: 'failure-string', + task_name: testCaseErrorDetails.task_bpmn_name, + task_id: testCaseErrorDetails.task_bpmn_identifier, + line_number: testCaseErrorDetails.task_line_number, + error_line: testCaseErrorDetails.task_line_contents, + task_trace: testCaseErrorDetails.task_trace, + stacktrace: testCaseErrorDetails.stacktrace, + }; + return errorForDisplay; +}; + export const childrenForErrorObject = (errorObject: ErrorForDisplay) => { let sentryLinkTag = null; if (errorObject.sentry_link) { diff --git a/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx b/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx index c6747857..fe82167e 100644 --- a/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx +++ b/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx @@ -3,19 +3,24 @@ import { Button, Modal } from '@carbon/react'; import { useState } from 'react'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import HttpService from '../services/HttpService'; -import { ProcessFile } from '../interfaces'; +import { ProcessFile, TestCaseResult, TestCaseResults } from '../interfaces'; +import { + childrenForErrorObject, + errorForDisplayFromTestCaseErrorDetails, +} from './ErrorDisplay'; type OwnProps = { processModelFile: ProcessFile; }; export default function ProcessModelTestRun({ processModelFile }: OwnProps) { - const [testCaseResults, setTestCaseResults] = useState(null); + const [testCaseResults, setTestCaseResults] = + useState(null); const [showTestCaseResultsModal, setShowTestCaseResultsModal] = useState(false); const { targetUris } = useUriListForPermissions(); - const onProcessModelTestRunSuccess = (result: any) => { + const onProcessModelTestRunSuccess = (result: TestCaseResults) => { setTestCaseResults(result); }; @@ -60,18 +65,80 @@ export default function ProcessModelTestRun({ processModelFile }: OwnProps) { }); }; + const testCaseFormattedResultTag = () => { + if (!testCaseResults) { + return null; + } + + const passingRows: any[] = []; + const failingRows: any[] = []; + + testCaseResults.passing.forEach((testCaseResult: TestCaseResult) => { + passingRows.push(

{testCaseResult.test_case_identifier}

); + }); + + testCaseResults.failing + .slice(0, 2) + .forEach((testCaseResult: TestCaseResult) => { + if (testCaseResult.test_case_error_details) { + const errorForDisplay = errorForDisplayFromTestCaseErrorDetails( + testCaseResult.test_case_error_details + ); + const errorChildren = childrenForErrorObject(errorForDisplay); + failingRows.push( + <> +
+

+ Test Case:{' '} + {testCaseResult.test_case_identifier} +

+ {errorChildren} + + ); + } + }); + + return ( + <> +

Passing: {testCaseResults.passing.length}

+

Failing: {testCaseResults.failing.length}

+
+ {failingRows.length > 0 ? ( + <> +

Failure Details:

+ {failingRows} + + ) : null} + {passingRows.length > 0 ? ( + <> +

Successful Test Cases:

+ {passingRows} + + ) : null} + + ); + }; + const testCaseResultsModal = () => { + if (!testCaseResults) { + return null; + } + + let modalHeading = 'All Tests PASSED'; + if (!testCaseResults.all_passed) { + modalHeading = 'Some Tests FAILED'; + } return ( setShowTestCaseResultsModal(false)} onRequestClose={() => setShowTestCaseResultsModal(false)} > - {JSON.stringify(testCaseResults)} + {testCaseFormattedResultTag()} ); }; diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 02a98367..81a56326 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -365,3 +365,26 @@ export interface InterstitialPageResponse { task?: ProcessInstanceTask; process_instance?: ProcessInstance; } + +export interface TestCaseErrorDetails { + error_messages: string[]; + stacktrace?: string[]; + task_bpmn_identifier?: string; + task_bpmn_name?: string; + task_line_contents?: string; + task_line_number?: number; + task_trace?: string[]; +} + +export interface TestCaseResult { + bpmn_file: string; + passed: boolean; + test_case_identifier: string; + test_case_error_details?: TestCaseErrorDetails; +} + +export interface TestCaseResults { + all_passed: boolean; + failing: TestCaseResult[]; + passing: TestCaseResult[]; +}