'use strict' ; import { workspace , TreeView , window , commands , ViewColumn , Uri , ProgressLocation , SaveDialogOptions , Position , Disposable , ExtensionContext } from 'vscode' ; import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper" ; import { dirname } from 'path' ; import { readFileSync } from 'fs' ; import { findNodeAtLocation , parseTree } from 'jsonc-parser' ; import { URI } from 'vscode-uri' ; import { PddlExtensionContext , Plan , PlanStep , parser , utils } from 'pddl-workspace' ; import { Test , TestOutcome } from './Test' ; import { PTestTreeDataProvider , PTestNode , PTestNodeKind } from './PTestTreeDataProvider' ; import { GeneratedDocumentContentProvider } from './GeneratedDocumentContentProvider' ; import { Planning } from '../planning/planning' ; import { PlanningOutcome , PlanningResult } from '../planning/PlanningResult' ; import { TestsManifest } from './TestsManifest' ; import { PTestReport } from './PTestReport' ; import { showError , jsonNodeToRange } from '../utils' ; import { CodePddlWorkspace } from '../workspace/CodePddlWorkspace' ; import { PTEST_VIEW_PROBLEM , PTEST_VIEW , PTEST_REVEAL } from './PTestCommands' ; import { DEFAULT_EPSILON } from '../configuration/configuration' ; import { ManifestGenerator } from './ManifestGenerator' ; import { PDDL_SAVE_AS_EXPECTED_PLAN } from '../planView/PlanView' ; /**
 * PDDL Test Explorer pane.
 */ export class PTestExplorer { private generatedDocumentContentProvider : GeneratedDocumentContentProvider ; private pTestViewer : TreeView < PTestNode > ; private pTestTreeDataProvider : PTestTreeDataProvider ; private report : PTestReport ; manifestGenerator : ManifestGenerator ; constructor ( readonly context , private readonly pddlContext , private readonly codePddlWorkspace , private readonly planning ) { this . pTestTreeDataProvider = new PTestTreeDataProvider ( pddlContext ) ; this . pTestViewer = window . createTreeView ( 'pddl.tests.explorer' , { treeDataProvider : this . pTestTreeDataProvider , showCollapseAll : true } ) ; this . subscribe ( this . pTestViewer ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( 'pddl.tests.refresh' , ( ) => this . pTestTreeDataProvider . refresh ( ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( 'pddl.tests.run' , node => this . runTest ( node ) . catch ( showError ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( 'pddl.tests.runAll' , node => this . findAndRunTests ( node ) . catch ( showError ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( PTEST_VIEW , nodeOrUri => { if ( nodeOrUri instanceof Uri ) { this . openTestByUri ( nodeOrUri as Uri ) . catch ( showError ) ; } else { this . openTest ( nodeOrUri ) . catch ( showError ) ; } } ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( PTEST_VIEW_PROBLEM , test => this . openProblemFile ( test , ViewColumn . Beside ) . catch ( showError ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( 'pddl.tests.viewDefinition' , node => this . openDefinition ( node ) . catch ( showError ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( 'pddl.tests.viewExpectedPlans' , node => this . openExpectedPlans ( node ) . catch ( showError ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( 'pddl.tests.problemSaveAs' , ( ) => this . saveProblemAs ( ) . catch ( showError ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( PTEST_REVEAL , nodeUri => this . pTestViewer . reveal ( this . pTestTreeDataProvider . findNodeByResourceOrThrow ( nodeUri ) , { select : true , expand : true } ) . then ( ( ) => { return ; } , showError ) ) ) ; this . manifestGenerator = new ManifestGenerator ( this . codePddlWorkspace . pddlWorkspace , this . pddlContext ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( 'pddl.tests.createAll' , ( ) => this . generateAllManifests ( ) . catch ( showError ) ) ) ; this . subscribe ( instrumentOperationAsVsCodeCommand ( PDDL_SAVE_AS_EXPECTED_PLAN , plan => this . manifestGenerator . createPlanAssertion ( plan ) . catch ( showError ) ) ) ; this . subscribe ( this . report = new PTestReport ( context , this . planning . output ) ) ; this . generatedDocumentContentProvider = new GeneratedDocumentContentProvider ( this . planning . output , this . codePddlWorkspace ) ; this . subscribe ( workspace . registerTextDocumentContentProvider ( 'tpddl' , this . generatedDocumentContentProvider ) ) ; } getTreeDataProvider ( ) { return this . pTestTreeDataProvider ; } async generateAllManifests ( ) { const manifests = await this . manifestGenerator . generateAll ( ) ; this . getTreeDataProvider ( ) . refresh ( ) ; return manifests ; } private subscribe ( disposable ) { this . pddlContext . subscriptions . push ( disposable ) ; } async saveProblemAs ( ) { const generatedDocument = window . activeTextEditor ?. document ; if ( generatedDocument === undefined ) { return ; } const options = { saveLabel : "Save as PDDL problem" , filters : { "PDDL" : [ "pddl" ] } , defaultUri : generatedDocument . uri . with ( { scheme : 'file' } ) } ; try { const uri = await window . showSaveDialog ( options ) ; if ( ! uri ) { return ; } const newDocument = await workspace . openTextDocument ( uri . with ( { scheme : 'untitled' } ) ) ; const editor = await window . showTextDocument ( newDocument , window . activeTextEditor ?. viewColumn ) ; await editor . edit ( edit => edit . insert ( new Position ( 0 , 0 ) , generatedDocument . getText ( ) ) ) ; } catch ( ex ) { console . log ( ex ) ; } } async openDefinition ( node ) { if ( node . kind === PTestNodeKind . Test ) { const test = Test . fromUri ( node . resource , this . pddlContext ) ; if ( test === undefined ) { throw new Error ( "No test found at: " + node . resource ) ; } const manifest = test . getManifest ( ) ; if ( ! manifest ) { throw new Error ( `Test ${ test . getLabel ( ) } is not associated to any manifest` ) ; } const manifestDocument = await workspace . openTextDocument ( manifest . uri ) ; const manifestText = ( await workspace . fs . readFile ( Uri . file ( manifest . path ) ) ) . toString ( ) ; const rootNode = parseTree ( manifestText ) ; const jsonTestNode = rootNode && findNodeAtLocation ( rootNode , [ "cases" , test . getIndex ( ) ?? 0 ] ) ; const selection = jsonTestNode && jsonNodeToRange ( manifestDocument , jsonTestNode ) ; await window . showTextDocument ( manifestDocument . uri , { preview : true , viewColumn : ViewColumn . One , selection : selection } ) ; } else if ( node . kind === PTestNodeKind . Manifest ) { const manifestDocument = await workspace . openTextDocument ( node . resource ) ; await window . showTextDocument ( manifestDocument . uri , { preview : true , viewColumn : ViewColumn . One } ) ; } } async openExpectedPlans ( node ) { if ( node . kind === PTestNodeKind . Test ) { const test = Test . fromUri ( node . resource , this . pddlContext ) ; if ( ! test ) { return ; } await this . assertValid ( test ) ; if ( ! test . hasExpectedPlans ( ) ) { await window . showInformationMessage ( "Test has no expected plans defined." ) ; } else { const previewOnly = test . getExpectedPlans ( ) . length === 1 ; test . getExpectedPlans ( ) . forEach ( async ( expectedPlan ) => { const path = test . toAbsolutePath ( expectedPlan ) ; const planDocument = await workspace . openTextDocument ( path ) ; await window . showTextDocument ( planDocument . uri , { preview : previewOnly , viewColumn : ViewColumn . Three } ) ; } ) ; } } } async openTest ( node ) { if ( node . kind === PTestNodeKind . Test ) { this . openTestByUri ( node . resource ) ; } } async openTestByUri ( testCaseUri ) { const test = Test . fromUri ( testCaseUri , this . pddlContext ) ; if ( ! test ) { return ; } await this . assertValid ( test ) ; const domainDocument = await workspace . openTextDocument ( test . getDomainUri ( ) ) ; await window . showTextDocument ( domainDocument . uri , { preview : true , viewColumn : ViewColumn . One } ) ; await this . openProblemFile ( test , ViewColumn . Two ) ; } async openProblemFile ( test , column ) { let uri ; if ( test . getPreProcessor ( ) ) { uri = this . generatedDocumentContentProvider . mapUri ( test ) ; this . generatedDocumentContentProvider . changed ( uri ) ; } else { const problemDocument = await workspace . openTextDocument ( test . getProblemUri ( ) ) ; uri = problemDocument . uri ; } await window . showTextDocument ( uri , { preview : true , viewColumn : column } ) ; } param node async findAndRunTests ( node ) { const allManifests = [ ] ; await this . findTests ( node , allManifests ) ; const contextPath = node ? workspace . asRelativePath ( node . resource . fsPath ) : "this workspace" ; this . runTests ( allManifests , contextPath ) ; } param node param allManifests async findTests ( node , allManifests ) { if ( ! node || node . kind === PTestNodeKind . Directory ) { const children = await this . pTestTreeDataProvider . getChildren ( node ) ; for ( const child of children ) { await this . findTests ( child , allManifests ) ; } } else if ( node . kind === PTestNodeKind . Manifest ) { const manifest = TestsManifest . load ( node . resource . fsPath , this . pddlContext ) ; allManifests . push ( manifest ) ; } } private runTests ( manifests , contextPath ) { const testCount = manifests . map ( m => m . testCases . length ) . reduce ( ( previousValue , currentValue ) => previousValue + currentValue , 0 ) ; this . report . clearAndShow ( ) ; window . withProgress ( { location : ProgressLocation . Notification , title : `Running tests from ${ contextPath }` , cancellable : true } , ( progress , token ) => { return new Promise ( async ( resolve , reject ) => { try { for ( const manifest of manifests ) { this . report . startingManifest ( manifest ) ; for ( let caseIndex = 0 ; caseIndex < manifest . testCases . length ; caseIndex ++ ) { if ( token . isCancellationRequested ) { this . planning . handleOutput ( 'Canceled by user.\n' ) ; reject ( ) ; break ; } const test = manifest . testCases [ caseIndex ] ; progress . report ( { message : 'Test case: ' + test . getLabel ( ) , increment : 100.0 / testCount } ) ; try { await this . executeTest ( test ) ; } catch ( e ) { console . log ( e ) ; } } this . report . finishedManifest ( manifest ) ; } resolve ( null ) ; } finally { this . report . show ( ) ; } } ) ; } ) ; } async runTest ( node ) { if ( node . kind === PTestNodeKind . Test ) { const test = Test . fromUri ( node . resource , this . pddlContext ) ; if ( ! test ) { return ; } try { await this . executeTest ( test ) ; } catch ( e ) { console . log ( e ) ; } } } executeTest ( test ) { let problemUri ; if ( test . getPreProcessor ( ) ) { problemUri = this . generatedDocumentContentProvider . mapUri ( test ) ; this . generatedDocumentContentProvider . changed ( problemUri ) ; } else { problemUri = test . getProblemUri ( ) ; } this . setTestOutcome ( test , TestOutcome . IN_PROGRESS ) ; return new Promise ( async ( resolve , reject ) => { try { const testValid = await this . assertValid ( test ) ; if ( ! testValid ) { this . outputTestResult ( test , TestOutcome . SKIPPED , Number . NaN , "Invalid test definition" ) ; reject ( new Error ( 'Invalid test ' + test . getLabel ( ) ) ) ; return ; } } catch ( ex ) { reject ( ex ) ; return ; } const resultSubscription = this . planning . onPlansFound ( result => { resultSubscription . dispose ( ) ; if ( result . outcome === PlanningOutcome . FAILURE ) { this . outputTestResult ( test , TestOutcome . FAILED , result . elapsedTime , result . error ) ; reject ( new Error ( result . error ?? "Unknown error while planning." ) ) ; return ; } else if ( result . outcome === PlanningOutcome . KILLED ) { this . outputTestResult ( test , TestOutcome . SKIPPED , result . elapsedTime , 'Killed by the user.' ) ; resolve ( false ) ; return ; } else if ( result . plans . length === 0 ) { this . outputTestResult ( test , TestOutcome . FAILED , result . elapsedTime , 'No plan found.' ) ; resolve ( false ) ; return ; } if ( test . hasExpectedPlans ( ) ) { try { this . assertMatchesAnExpectedPlan ( result , test ) ; } catch ( err ) { const error = err as Error ; this . outputTestResult ( test , TestOutcome . FAILED , result . elapsedTime , "Failed to compare plan to expected plans. Error: " + error . message ?? err ) ; } } else { this . outputTestResult ( test , TestOutcome . SUCCESS , result . elapsedTime ) ; } resolve ( true ) ; } ) ; try { const cwd = test . getManifest ( ) ? dirname ( test . getManifest ( ) ! . path ) : '.' ; await commands . executeCommand ( 'pddl.planAndDisplayResult' , test . getDomainUri ( ) , problemUri , cwd , test . getOptions ( ) ) ; } catch ( e ) { this . setTestOutcome ( test , TestOutcome . FAILED ) ; reject ( e ) ; return ; } } ) ; } private assertMatchesAnExpectedPlan ( result , test ) { const success = result . plans . every ( plan => test . getExpectedPlans ( ) . map ( expectedPlanFileName => test . toAbsolutePath ( expectedPlanFileName ) ) . some ( expectedPlanPath => this . areSame ( plan , this . loadPlan ( expectedPlanPath ) ) ) ) ; if ( success ) { this . outputTestResult ( test , TestOutcome . SUCCESS , result . elapsedTime ) ; } else { this . outputTestResult ( test , TestOutcome . FAILED , result . elapsedTime , "Actual plan is NOT matching any of the expected plans." ) ; } } outputTestResult ( test , outcome , elapsedTime , error ? ) { this . setTestOutcome ( test , outcome ) ; this . report . outputTestResult ( test , outcome , elapsedTime , error ) ; } setTestOutcome ( test , testOutcome ) { this . pTestTreeDataProvider . setTestOutcome ( test , testOutcome ) ; } areSame ( actualPlan , expectedPlan ) { if ( actualPlan . steps . length !== expectedPlan . steps . length ) { return false ; } if ( actualPlan . steps . length === 0 ) { return true ; } const epsilon = workspace . getConfiguration ( ) . get < number > ( "pddlPlanner.epsilonTimeStep" , DEFAULT_EPSILON ) ; if ( ! PlanStep . equalsWithin ( actualPlan . makespan , expectedPlan . makespan , epsilon ) ) { return false ; } for ( let index = 0 ; index < actualPlan . steps . length ; index ++ ) { const actualStep = actualPlan . steps [ index ] ; const expectedStep = expectedPlan . steps [ index ] ; if ( ! expectedStep . equals ( actualStep , epsilon ) ) { return false ; } } return true ; } loadPlan ( expectedPlanPath ) { const expectedPlanText = readFileSync ( expectedPlanPath , { encoding : "utf-8" } ) ; const epsilon = workspace . getConfiguration ( ) . get < number > ( "pddlPlanner.epsilonTimeStep" , DEFAULT_EPSILON ) ; return parser . PddlPlannerOutputParser . parseOnePlan ( expectedPlanText , URI . file ( expectedPlanPath ) , epsilon ) ; } async assertValid ( test ) { const domainExists = await this . assertFileExists ( test . getDomainUri ( ) . fsPath , 'Domain' , test . getDomain ( ) ) ; const problemExists = await this . assertFileExists ( test . getProblemUri ( ) . fsPath , 'Problem' , test . getProblem ( ) ) ; const expectedPlanPromises = test . getExpectedPlans ( ) . map ( async ( planPath ) => await this . assertFileExists ( test . toAbsolutePath ( planPath ) , 'Test' , planPath ) ) ; const expectedPlansExist = await Promise . all ( expectedPlanPromises ) ; return domainExists && problemExists && expectedPlansExist . every ( v => v ) ; } async assertFileExists ( path , resourceName , fileName ) { const exists = await utils . afs . exists ( path ) ; if ( ! exists ) { window . showErrorMessage ( `${ resourceName } file not found: ${ fileName }` ) ; } return exists ; } }