Restoring Corrupted ATF Step Order: A Scripted Recovery Strategy

Corey19
Tera Expert

You may've seen this, a glitch causes an Automated Test Framework (ATF) test to lose its step ordering. Suddenly, Step 5 is Step 1,  or you have two Step 22s etc and the test is unusable. Manually reordering dozens of steps is tedious and prone to human error.

https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0861763
Rather than accepting "This is expected behavior and by design in all currently supported releases." and using the provided remedy...

  • To remove duplicate 'Execution order', Use the 'Copy Test' button and save the copied test.
  • To remove any duplicate steps, select the step then using the 'action on selected rows...' choose delete.
  • Check the flow of the test and reorder as required.

...we can take a far simpler approach.

Instead of guessing the order, I wrote a background script to recover the correct sequence from the last successful execution of the test. By using the sys_atf_test_result table as the "source of truth," we can mathematically guarantee the steps are restored to their working order.

 

The script operates in five distinct phases to ensure data integrity:

  1. It locates the most recent "Success" result for the target test and maps the historical execution order.
  2. It performs a "Strict Match" check. It compares the step count and Sys IDs of the historical run against the current test definition. If the test has been modified (steps added/removed) since the last run, the script aborts to prevent damage.
  3. It sets the order field of all current steps to NULL.
  4. It applies the recorded order from the history. Crucially, it enables workflows (setWorkflow(true)) during this phase so the changes are captured in your active Update Set.
  5. It reads the data back one last time to ensure the platform hasn't auto-sorted the steps incorrectly.

The Script Note: Please run this in a Sub-Production environment first. You must replace REPLACE_WITH_SYSID_OF_TEST with the Sys ID of the corrupted test.

 

(function() {
    // ------------------------------------------------------------------
    // CONFIGURATION
    // ------------------------------------------------------------------
    var TARGET_TEST_SYS_ID = 'REPLACE_WITH_SYSID_OF_TEST'; 
    
    // ------------------------------------------------------------------
    // 1. FETCH: Find the last successful run
    // ------------------------------------------------------------------
    gs.info('--- PHASE 1: SEARCHING FOR LAST SUCCESSFUL RUN ---');
    
    var resultGr = new GlideRecord('sys_atf_test_result');
    resultGr.addQuery('test', TARGET_TEST_SYS_ID);
    resultGr.addQuery('status', 'success'); 
    resultGr.orderByDesc('sys_created_on'); 
    resultGr.setLimit(1);
    resultGr.query();

    if (!resultGr.next()) {
        gs.error('ABORTING: No successful test result found for Test ID: ' + TARGET_TEST_SYS_ID);
        return;
    }

    gs.info('Found Reference Run: ' + resultGr.getDisplayValue() + ' (' + resultGr.sys_created_on + ')');

    // ------------------------------------------------------------------
    // 2. MAP & VALIDATE: Compare History vs. Current Reality
    // ------------------------------------------------------------------
    gs.info('--- PHASE 2: VALIDATING INTEGRITY ---');

    var historyMap = {};
    var historyCount = 0;
    
    // Query the RESULT steps using the confirmed 'order' field
    var resultStepGr = new GlideRecord('sys_atf_test_result_step');
    resultStepGr.addQuery('test_result', resultGr.getUniqueValue());
    resultStepGr.orderBy('order'); 
    resultStepGr.query();

    while (resultStepGr.next()) {
        var stepId = resultStepGr.step.toString();
        if (stepId) {
            // Store order as integer for accurate comparison later
            historyMap[stepId] = parseInt(resultStepGr.getValue('order'), 10);
            historyCount++;
        }
    }
    
    gs.info('Mapped ' + historyCount + ' steps from history.');

    // B. Analyze Current Test Definition
    var currentStepGr = new GlideRecord('sys_atf_step');
    currentStepGr.addQuery('test', TARGET_TEST_SYS_ID);
    currentStepGr.query();
    
    var currentCount = currentStepGr.getRowCount();
    var matchedCount = 0;

    // CHECK 1: Count Mismatch
    if (historyCount !== currentCount) {
        gs.error('ABORTING: Count Mismatch. Historical run had ' + historyCount + ' steps, but current test has ' + currentCount + '.');
        return;
    }

    // CHECK 2 & 3: ID Match & Active State
    while (currentStepGr.next()) {
        var currentId = currentStepGr.getUniqueValue();
        
        if (!historyMap.hasOwnProperty(currentId)) {
            gs.error('ABORTING: Unknown Step. Step "' + currentStepGr.step_config.getDisplayValue() + '" exists now but was not in the reference run.');
            return;
        }

        if (currentStepGr.active == false) {
             gs.error('ABORTING: Inactive Step. Step "' + currentStepGr.step_config.getDisplayValue() + '" matches history but is currently Inactive.');
             return;
        }

        matchedCount++;
    }

    if (matchedCount !== historyCount) {
        gs.error('ABORTING: Validation Logic Error. Matched count (' + matchedCount + ') does not equal history count (' + historyCount + ').');
        return;
    }

    gs.info('VALIDATION PASSED: Current test structure matches historical execution.');

    // ------------------------------------------------------------------
    // 3. FLUSH: Nullify Orders
    // ------------------------------------------------------------------
    gs.info('--- PHASE 3: NULLIFYING EXISTING ORDERS ---');
    
    var flushGr = new GlideRecord('sys_atf_step');
    flushGr.addQuery('test', TARGET_TEST_SYS_ID);
	flushGr.orderBy('order');
    flushGr.query();
    
    while (flushGr.next()) {
		gs.info('Step ' + flushGr.order + ' ' + flushGr.step_config.getDisplayValue() + ' set as NULL.');
        flushGr.setValue('order', null); 
        flushGr.update();
		gs.info('Read order back - ' + flushGr.getValue('order'));
    }
    gs.info('Existing step orders set to NULL.');
	
		// ------------------------------------------------------------------
		// 4. FIX: Apply Recorded Order (WITH WORKFLOWS)
		// ------------------------------------------------------------------
		gs.info('--- PHASE 4: RESTORING HISTORICAL ORDER (CAPTURING IN UPDATE SET) ---');
		
		for (var id in historyMap) {
			var restoreGr = new GlideRecord('sys_atf_step');
			if (restoreGr.get(id)) {
				restoreGr.order = historyMap[id];
				restoreGr.setWorkflow(true); 
				restoreGr.update();
			}
		}

		// ------------------------------------------------------------------
		// 5. VERIFY: Compare Final State vs History Map
		// ------------------------------------------------------------------
		gs.info('--- PHASE 5: STRICT VERIFICATION ---');
		
		var verifyGr = new GlideRecord('sys_atf_step');
		verifyGr.addQuery('test', TARGET_TEST_SYS_ID);
		verifyGr.orderBy('order');
		verifyGr.query();
		
		var verificationFailures = 0;
		
		while (verifyGr.next()) {
			var verifyId = verifyGr.getUniqueValue();
			var actualOrder = parseInt(verifyGr.getValue('order'), 10);
			var expectedOrder = historyMap[verifyId];
			
			if (actualOrder !== expectedOrder) {
				gs.error('FAILURE: Step "' + verifyGr.step_config.getDisplayValue() + '" has Order ' + actualOrder + ' but expected ' + expectedOrder);
				verificationFailures++;
			} else {
				gs.info('MATCH: Order ' + actualOrder + ' confirmed for step ' + verifyGr.step_config.getDisplayValue());
			}
		}
		
		if (verificationFailures > 0) {
			gs.error('--- RESTORATION FAILED WITH ' + verificationFailures + ' ERRORS ---');
		} else {
			gs.info('--- RESTORATION SUCCESSFUL: All steps match historical order ---');
		}
})();

 

0 REPLIES 0