import * as vscode from 'vscode' ; import { DebugSessionClass } from '../debugadapter' ; import { RemoteFactory } from '../remotes/remotefactory' ; import { Remote , RemoteBreakpoint } from '../remotes/remotebase' ; import { Labels , SourceFileEntry } from '../labels/labels' ; import { Settings } from '../settings' ; import { Utility } from '../misc/utility' ; import { Decoration } from '../decoration' ; import { StepHistory , CpuHistory , CpuHistoryClass } from '../remotes/cpuhistory' ; import { Z80RegistersClass , Z80Registers } from '../remotes/z80registers' ; import { StepHistoryClass } from '../remotes/stephistory' ; import { ZSimRemote } from '../remotes/zsimulator/zsimremote' ; import { UnitTestCaseBase , UnitTestCase , RootTestSuite , UnitTestSuiteConfig , UnitTestSuite } from './unittestcase' ; import { PromiseCallbacks } from '../misc/promisecallbacks' ; import { DiagnosticsHandler } from '../diagnosticshandler' ; import { GenericWatchpoint } from '../genericwatchpoint' ; import { ZxNextSocketRemote } from '../remotes/dzrpbuffer/zxnextsocketremote' ; /**
 * This class takes care of executing the unit tests.
 * It basically
 * 1. Reads the list file to find the unit test labels.
 * 2. Loads the binary into the emulator.
 * 3. Manipulates memory and PC register to call a specific unit test.
 * 4. Loops over all found unit tests.
 */ export class Z80UnitTestRunner { protected static addrStart : number ; protected static addrTestWrapper : number ; protected static addrCall : number ; protected static addrTestReadySuccess : number ; protected static stackBottom : number ; protected static stackTop : number ; protected static currentFail : boolean ; protected static debug = false ; protected static debugAdapter : DebugSessionClass ; protected static rootTestSuite : RootTestSuite ; protected static testController : vscode . TestController ; protected static testConfig : UnitTestSuiteConfig | undefined ; protected static waitOnDebugger : PromiseCallbacks < void > | undefined ; protected static currentTestRun : vscode . TestRun | undefined ; protected static currentTestItem : vscode . TestItem | undefined ; protected static currentTestStart : number ; protected static currentTestFailed : boolean ; protected static testCaseSetup : boolean ; protected static timedOut : boolean ; protected static testRunActive : boolean ; protected static stoppingTests : boolean ; /**
     * Called to initialize the test controller.
     */ public static Init ( ) { this . testConfig = undefined ; this . testRunActive = false ; this . testController = vscode . tests . createTestController ( 'maziac.dezog.z80unittest.controller' , 'Z80 Unit Tests' ) ; this . rootTestSuite = new RootTestSuite ( this . testController ) ; this . testController . createRunProfile ( 'Run' , vscode . TestRunProfileKind . Run , ( request , token ) => { this . runHandler ( request , token ) ; } ) ; this . testController . createRunProfile ( 'Debug' , vscode . TestRunProfileKind . Debug , ( request , token ) => { this . runDebugHandler ( request , token ) ; } ) ; } /**
     * Runs one or several test cases. (Not debug)
     */ protected static async runHandler ( request , token ) { this . debug = false ; await this . runOrDebugHandler ( request , token ) ; } /**
     * Runs one or several test cases. (debug)
     */ protected static async runDebugHandler ( request , token ) { this . debug = true ; await this . runOrDebugHandler ( request , token ) ; } param request protected static async runOrDebugHandler ( request , token ) { DiagnosticsHandler . clear ( ) ; if ( this . testRunActive ) { await this . stopUnitTests ( ) ; return ; } this . stoppingTests = false ; this . testRunActive = true ; const run = this . testController . createTestRun ( request ) ; this . currentTestRun = run ; const queue = [ ] ; token . onCancellationRequested ( async ( ) => { await this . stopUnitTests ( ) ; } ) ; this . testConfig = undefined ; if ( request . include ) { request . include . forEach ( test => queue . push ( test ) ) ; } else { this . testController . items . forEach ( test => queue . push ( test ) ) ; } while ( ! this . stoppingTests && queue . length > 0 && ! token . isCancellationRequested ) { const test = queue . shift ( ) ! ; if ( request . exclude ?. includes ( test ) ) continue ; if ( test . children . size > 0 ) { const tmp = [ ] ; test . children . forEach ( item => tmp . push ( item ) ) ; queue . unshift ( ... tmp ) ; } const ut = UnitTestCaseBase . getUnitTestCase ( test ) as UnitTestCase ; if ( ! ( ut instanceof UnitTestSuite ) ) { let timeoutHandle ; this . timedOut = false ; this . currentTestStart = Date . now ( ) ; try { if ( this . debug ) await this . setupDebugTestCase ( ut ) ; else await this . setupRunTestCase ( ut ) ; } catch ( e ) { vscode . window . showErrorMessage ( e . message ) ; break ; } try { if ( ! this . debug && ! ( Remote instanceof ZxNextSocketRemote ) ) { const toMs = 1000 * Settings . launch . unitTestTimeout ; if ( toMs > 0 ) { timeoutHandle = setTimeout ( ( ) => { this . timedOut = true ; Remote . pause ( ) ; } , toMs ) ; } } this . currentTestItem = test ; this . currentTestStart = Date . now ( ) ; run . started ( test ) ; await this . runTestCase ( ut ) ; } catch ( e ) { if ( ! this . stoppingTests ) { this . testFailed ( e . message , e . position ) ; } } finally { clearTimeout ( timeoutHandle ) ; this . currentTestItem = undefined ; } } } run . end ( ) ; if ( Remote ) { await this . stopUnitTests ( ) ; } this . stoppingTests = true ; this . testRunActive = false ; } /**
     * Sets up the test case.
     * Goes up the parents until it finds the unit test config.
     * Then (if not yet done before) it starts up the Remote to be
     * able to execute a single test case.
     */ protected static async setupRunTestCase ( ut ) { const testConfig = ut . getConfigParent ( ) ; if ( ! testConfig ) throw Error ( "No test config found." ) ; if ( this . testConfig == testConfig ) return ; this . testConfig = testConfig ; await this . terminateRemote ( ) ; this . debugAdapter = undefined as any ; const configuration = this . testConfig . config ; Settings . launch = Settings . Init ( configuration ) ; Settings . CheckSettings ( ) ; Utility . setRootPath ( Settings . launch . rootFolder ) ; Decoration . clearAllDecorations ( ) ; Z80RegistersClass . createRegisters ( ) ; RemoteFactory . createRemote ( configuration . remoteType ) ; if ( ! ( CpuHistory as any ) ) { CpuHistoryClass . setCpuHistory ( new StepHistoryClass ( ) ) ; StepHistory . decoder = Z80Registers . decoder ; } Labels . init ( configuration . smallValuesMaximum ) ; Remote . readListFiles ( configuration ) ; return new Promise < void > ( async ( resolve , reject ) => { Remote . once ( 'initialized' , async ( ) => { try { StepHistory . init ( ) ; await Remote . enableLogpointGroup ( undefined , true ) ; try { await Remote . enableWPMEM ( true ) ; } catch ( e ) { vscode . window . showWarningMessage ( e . message ) ; } await Remote . enableAssertionBreakpoints ( true ) ; if ( this . debug ) { if ( Settings . launch . startAutomatically ) await Utility . timeout ( 500 ) ; } await this . initUnitTests ( ) ; resolve ( ) ; } catch ( e ) { reject ( e ) ; } } ) ; Remote . on ( 'coverage' , coveredAddresses => { Decoration . showCodeCoverage ( coveredAddresses ) ; } ) ; Remote . on ( 'warning' , message => { vscode . window . showWarningMessage ( message ) ; } ) ; Remote . on ( 'debug_console' , message => { vscode . debug . activeDebugConsole . appendLine ( message ) ; } ) ; Remote . once ( 'error' , e => { Remote ?. dispose ( ) ; reject ( e ) ; } ) ; try { await Remote . init ( ) ; } catch ( e ) { reject ( e ) ; } } ) ; } /**
     * Sets up the test case.
     * Goes up the parents until it finds the unit test config.
     * Then (if not yet done before) it starts up the debug adapter.
     */ protected static async setupDebugTestCase ( ut ) { const testConfig = ut . getConfigParent ( ) ; if ( ! testConfig ) throw Error ( "No test config found." ) ; if ( this . testConfig == testConfig ) return ; this . testConfig = testConfig ; await this . terminateRemote ( ) ; Utility . setRootPath ( testConfig . wsFolder ) ; this . debugAdapter = undefined as any ; try { const configName = testConfig . testItem . label ; this . debugAdapter = await DebugSessionClass . unitTestsStart ( configName ) ; } catch ( e ) { throw e ; } try { await Remote . enableLogpointGroup ( undefined , true ) ; try { await Remote . enableWPMEM ( true ) ; } catch ( e ) { vscode . window . showWarningMessage ( e . message ) ; } await Remote . enableAssertionBreakpoints ( true ) ; await Utility . timeout ( 500 ) ; await this . initUnitTests ( ) ; await Remote . waitForBeingQuietFor ( 1000 ) ; } catch ( e ) { throw e ; } } /**
     * Checks if the debugger is active. If yes terminates it and
     * executes the unit tests.
     */ protected static async terminateRemote ( ) { return new Promise < void > ( async ( resolve ) => { if ( Remote ) { await Remote . terminate ( ) ; RemoteFactory . removeRemote ( ) ; } Utility . delayedCall ( time => { if ( time >= 5.0 ) { vscode . window . showErrorMessage ( 'Could not terminate active debug session. Please try manually.' ) ; resolve ( ) ; return true ; } if ( vscode . debug . activeDebugSession ) return false ; resolve ( ) ; return true ; } ) ; } ) ; } param test protected static async runTestCase ( ut ) { const utLabel = ut . utLabel ; if ( Remote instanceof ZSimRemote ) { const zsim = Remote ; zsim . customCode ?. execute ( utLabel ) ; } this . testCaseSetup = true ; this . currentTestFailed = false ; await this . execAddr ( this . addrStart ) ; if ( this . currentTestFailed ) return ; if ( this . timedOut ) { this . testFailed ( 'Timeout during test case setup.' ) ; return ; } const utAddr = this . getLongAddressForLabel ( utLabel ) ; let breakpoint ; if ( this . debug && ! Settings . launch . startAutomatically ) { breakpoint = { bpId : 0 , filePath : '' , lineNr : - 1 , address : utAddr , condition : '' , log : undefined } ; await Remote . setBreakpoint ( breakpoint ) ; } this . testCaseSetup = false ; await this . execAddr ( utAddr ) ; if ( breakpoint ) { if ( Remote ) await Remote . removeBreakpoint ( breakpoint ) ; } } /**
     * Initializes the unit tests. Is called after the Remote/the emulator has been setup.
     */ protected static async initUnitTests ( ) { this . addrStart = this . getLongAddressForLabel ( "UNITTEST_START" ) ; this . addrTestWrapper = this . getLongAddressForLabel ( "UNITTEST_TEST_WRAPPER" ) ; this . addrCall = this . getLongAddressForLabel ( "UNITTEST_CALL_ADDR" ) ; this . addrCall ++ ; this . addrTestReadySuccess = this . getLongAddressForLabel ( "UNITTEST_TEST_READY_SUCCESS" ) ; this . stackTop = this . getLongAddressForLabel ( "UNITTEST_STACK" ) ; try { this . stackBottom = this . getLongAddressForLabel ( "UNITTEST_STACK_BOTTOM" ) ; } catch ( e ) { throw Error ( "An error occurred with the 'UNITTEST_INITIALIZE' macro. Did you forget to update it for DeZog version 2.4?" ) ; } this . currentFail = true ; const opcode = await Remote . readMemory ( this . addrTestWrapper & 0xFFFF ) ; if ( opcode != 0xF3 ) throw Error ( "Code for unit tests is not present." ) ; const successBp = { bpId : 0 , filePath : '' , lineNr : - 1 , address : Z80UnitTestRunner . addrTestReadySuccess , condition : '' , log : undefined } ; await Remote . setBreakpoint ( successBp ) ; try { const stackMinWp = { address : this . stackBottom , size : 2 , access : 'rw' , condition : '' } ; const stackMaxWp = { address : this . stackTop , size : 2 , access : 'rw' , condition : '' } ; await Remote . setWatchpoint ( stackMinWp ) ; await Remote . setWatchpoint ( stackMaxWp ) ; } catch ( e ) { } } param label returns protected static getLongAddressForLabel ( label ) { const loc = Labels . getLocationOfLabel ( label ) ; let addr ; if ( loc ) addr = loc . address ; if ( addr == undefined ) { throw Error ( "Unit tests are not enabled in the enabled sources. Label " + label + " is not found. Did you forget to use the 'UNITTEST_INITIALIZE' macro ?" ) ; } return addr ; } param address protected static async execAddr ( address ) { const callAddr = new Uint8Array ( [ address & 0xFF , ( address >>> 8 ) & 0xFF ] ) ; await Remote . writeMemoryDump ( this . addrCall & 0xFFFF , callAddr ) ; const bank = Z80Registers . getBankFromAddress ( address ) ; if ( bank >= 0 ) { const slot = Z80Registers . getSlotFromAddress ( address ) ; await Remote . setSlot ( slot , bank ) ; } const addr64k = this . addrTestWrapper & 0xFFFF ; await Remote . setRegisterValue ( "PC" , addr64k ) ; StepHistory . clear ( ) ; await Remote . getRegistersFromEmulator ( ) ; await Remote . getCallStackFromEmulator ( ) ; await this . RemoteContinue ( ) ; } /**
     * Starts Continue directly or through the debug adapter.
     */ protected static async RemoteContinue ( ) { Remote . startProcessing ( ) ; if ( this . debugAdapter ) { this . debugAdapter . sendEventContinued ( ) ; const finish = new Promise < void > ( ( resolve , reject ) => { new PromiseCallbacks < void > ( this , 'waitOnDebugger' , resolve , reject ) ; } ) ; await this . debugAdapter . remoteContinue ( ) ; if ( Remote ) Remote . stopProcessing ( ) ; await finish ; console . log ( ) ; } else { let reasonString = await Remote . continue ( ) ; Remote . stopProcessing ( ) ; const pc = Remote . getPCLong ( ) ; if ( pc == this . addrTestReadySuccess ) { this . testPassed ( ) ; } else { if ( this . timedOut ) { reasonString = "Timeout (" + Settings . launch . unitTestTimeout + "s)" ; } this . testFailed ( reasonString ) ; } } } param breakReasonString returns public static async dbgCheckUnitTest ( breakReasonString ) { Utility . assert ( this . waitOnDebugger ) ; const pc = Remote . getPCLong ( ) ; if ( pc == this . addrTestReadySuccess ) { if ( ! this . currentTestFailed ) this . testPassed ( ) ; this . waitOnDebugger ! . resolve ( ) ; return true ; } else { if ( breakReasonString ?. toLowerCase ( ) . startsWith ( 'assertion' ) ) { this . testFailed ( breakReasonString ) ; } return false ; } } param reason param position protected static testFailed ( reason ? , position ? ) { const testMsg = new vscode . TestMessage ( reason || "Failure." ) ; if ( position ) { const uri = vscode . Uri . file ( position . filename ) ; const line = position . line ; const column = position . column ; const range = new vscode . Range ( line , column , line , column ) ; testMsg . location = new vscode . Location ( uri , range ) ; } else { const pc = Remote ?. getPCLong ( ) ; if ( pc != undefined ) { const positionPc = Labels . getFileAndLineForAddress ( pc ) ; if ( positionPc ) { const uri = vscode . Uri . file ( positionPc . fileName ) ; const line = positionPc . lineNr ; const range = new vscode . Range ( line , 10000 , line , 10000 ) ; testMsg . location = new vscode . Location ( uri , range ) ; } } } this . currentTestRun ?. failed ( this . currentTestItem ! , testMsg , Date . now ( ) - this . currentTestStart ) ; this . currentTestFailed = true ; } /**
     * Make the test item pass.
     */ protected static testPassed ( ) { if ( ! this . testCaseSetup ) { this . currentTestRun ?. passed ( this . currentTestItem ! , Date . now ( ) - this . currentTestStart ) ; } } /**
     *  Command to cancel the unit tests.
     *  Called from the debug adapter.
     */ public static async cancelUnitTests ( ) { this . stoppingTests = true ; if ( this . waitOnDebugger ) this . waitOnDebugger . reject ( Error ( "Unit test cancelled." ) ) ; } /**
     * Stops all unit tests.
     * Called by the test runner.
     */ protected static async stopUnitTests ( ) { return new Promise < void > ( async ( resolve ) => { this . stoppingTests = true ; if ( this . waitOnDebugger ) this . waitOnDebugger . reject ( Error ( "Unit tests cancelled" ) ) ; if ( this . debugAdapter ) { if ( Remote ) await Remote . waitForBeingQuietFor ( 300 ) ; } StepHistory . clear ( ) ; if ( Remote ) { await Remote . terminate ( ) ; Remote . removeAllListeners ( ) ; Remote . dispose ( ) ; } resolve ( ) ; } ) ; } }