Source: main.js

#!/usr/bin/env node
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
import {DiffConfig} from './config/DiffConfig.js';
import {Preprocessor} from './io/Preprocessor.js';
import {CpeeDiff} from './diff/CpeeDiff.js';
import {DiffEvaluation} from './eval/driver/DiffEvaluation.js';
import {MergeEvaluation} from './eval/driver/MergeEvaluation.js';
import {MatchingEvaluation} from './eval/driver/MatchingEvaluation.js';
import {EvalConfig} from './config/EvalConfig.js';
import * as fs from 'fs';
import {Logger} from './util/Logger.js';
import {CpeeMerge} from './merge/CpeeMerge.js';
import {MatchPipeline} from './diff/match/MatchPipeline.js';
import {Node} from './tree/Node.js';
import {GeneratedDiffEvaluation} from './eval/driver/GeneratedDiffEvaluation.js';
import {GeneratedMatchingEvaluation} from './eval/driver/GeneratedMatchingEvaluation.js';
import {EditScript} from './diff/delta/EditScript.js';
import {Patcher} from './diff/patch/Patcher.js';
import {DeltaTreeGenerator} from './diff/patch/DeltaTreeGenerator.js';
import {DiffTestResult} from './eval/result/DiffTestResult.js';
import {DiffTestCase} from './eval/case/DiffTestCase.js';
import {CpeeDiffLocalAdapter} from './eval/diff_adapters/CpeeDiffLocalAdapter.js';
import {markdownTable} from 'markdown-table';
import {fileURLToPath} from 'url';
import {dirname} from 'path';

/**
 * @file Main entrypoint for the CpeeDiff command line utility.
 */

const argv = yargs(hideBin(process.argv))
    .option('logLevel', {
      global: true,
      description: 'Choose the desired log level',
      alias: 'l',
      type: 'string',
      choices: Object.values(Logger.LOG_LEVELS),
      default: Logger.LOG_LEVELS.ERROR,
    })
    .command(
        'diff <old> <new>',
        'Calculate and show the difference between two CPEE process trees',
        (yargs) => {
          yargs
              .positional('old', {
                description: 'Path to the original CPEE process tree as an ' +
                    'XML document',
                type: 'string',
              })
              .positional('new', {
                description: 'Path to the changed CPEE process tree as an ' +
                    'XML document',
                type: 'string',
              })
              .option('mode', {
                description: 'Select the matching mode to use.',
                alias: 'm',
                type: 'string',
                choices: Object.values(MatchPipeline.MATCH_MODES),
                default: MatchPipeline.MATCH_MODES.QUALITY,
              })
              .option('threshold', {
                description: 'Define the threshold for matching nodes',
                alias: 't',
                type: 'number',
                default: 0.4,
              })
              .option('variablePrefix', {
                description: 'Specify the prefix used to detect read/written ' +
                    'variables in code and arguments',
                alias: 'v',
                type: 'string',
                default: 'data.',
              })
              .option('format', {
                description: 'Select the output format',
                alias: 'f',
                type: 'string',
                choices: [
                  'editScript',
                  'deltaTree',
                  'matching',
                  'summary',
                ],
                default: 'editScript',
              })
              .option('pretty', {
                description: 'Pretty-print the output XML document',
                alias: 'p',
                type: 'boolean',
                default: false,
              })
              .check((argv) => {
                if (!fs.existsSync(argv.old)) {
                  throw new Error(argv.old + ' ist not a valid file path');
                }
                if (!fs.existsSync(argv.new)) {
                  throw new Error(argv.new + ' ist not a valid file path');
                }
                if (argv.threshold < 0 || argv.threshold > 1) {
                  throw new Error('threshold must be in [0,1]');
                }
                return true;
              });
        },
        (argv) => {
          // Configure diff instance
          DiffConfig.VARIABLE_PREFIX = argv.variablePrefix;
          DiffConfig.COMPARISON_THRESHOLD = argv.threshold;
          DiffConfig.MATCH_MODE = argv.mode;
          DiffConfig.LOG_LEVEL = argv.logLevel;
          DiffConfig.PRETTY_XML = argv.pretty;

          const parser = new Preprocessor();
          const oldTree = parser.fromFile(argv.old);
          const newTree = parser.fromFile(argv.new);

          const editScript = new CpeeDiff().diff(
              oldTree,
              newTree,
          );

          Logger.info('Formatting result as ' + argv.format);
          switch (argv.format) {
            case 'editScript': {
              Logger.result(editScript.toXmlString());
              break;
            }
            case 'deltaTree': {
              const deltaTreeGen = new DeltaTreeGenerator();
              const deltaTree = deltaTreeGen.deltaTree(oldTree, editScript);
              Logger.result(deltaTree.toXmlString());
              break;
            }
            case 'matching': {
              const matching =
                  MatchPipeline.fromMode().execute(oldTree, newTree);
              Logger.result(matching.toXmlString());
              break;
            }
            case 'summary': {
              //
              Logger.result('#Nodes (old tree): ' + oldTree.size());
              Logger.result('#Nodes (new tree): ' + oldTree.size());
              const dummyCase = new DiffTestCase('main', oldTree, newTree);
              const result = new CpeeDiffLocalAdapter().evalCase(dummyCase);
              const table = [
                DiffTestResult.header(),
                result.values(),
              ];
              Logger.result(markdownTable(table));
              break;
            }
          }
        },
    )
    .command(
        'eval <suite>',
        'Evaluate CpeeDiff and compare against other algorithms',
        (yargs) => {
          yargs
              .positional('suite', {
                description: 'The evaluation suite to run',
                type: 'string',
                choices: [
                  'match',
                  'diff',
                  'merge',
                  'genDiff',
                  'genMatch',
                ],
              })
              .option('timeout', {
                description: 'The time limit for each individual test case ' +
                    'in seconds',
                alias: 't',
                type: 'number',
                default: 30,
              });
        },
        (argv) => {
          if (argv.pretty) {
            Logger.info('Overriding option "pretty" with false');
            argv.pretty = false;
          }
          DiffConfig.PRETTY_XML = argv.pretty;
          DiffConfig.LOG_LEVEL = argv.logLevel;
          EvalConfig.EXECUTION_OPTIONS.timeout = argv.timeout * 1000;
          EvalConfig.RUN_AUTOGENERATED_TESTS = argv.gen;

          // Adjust case directories
          const currFile = fileURLToPath(import.meta.url);
          const currDirectory = dirname(currFile);
          const pathPrefix = currDirectory + '/../';

          Logger.info('Executing test suite ' + argv.suite);
          switch (argv.suite) {
            case 'match': {
              MatchingEvaluation
                  .all()
                  .evalAll(pathPrefix + EvalConfig.MATCH_CASES_DIR);
              break;
            }
            case 'diff': {
              DiffEvaluation
                  .all()
                  .evalAll(pathPrefix + EvalConfig.MATCH_CASES_DIR);
              break;
            }
            case 'merge': {
              MergeEvaluation
                  .all()
                  .evalAll(pathPrefix + EvalConfig.MERGE_CASES_DIR);
              break;
            }
            case 'genDiff': {
              GeneratedDiffEvaluation.all().evalAll();
              break;
            }
            case 'genMatch': {
              GeneratedMatchingEvaluation.all().evalAll();
              break;
            }
          }
        },
    )
    .command(
        'merge <base> <branch1> <branch2>',
        'Perform a three-way merge for process trees',
        (yargs) => {
          yargs
              .positional('base', {
                description: 'Path to the base CPEE process tree as an ' +
                    'XML document',
                type: 'string',
              })
              .positional('branch1', {
                description: 'Path to the first branch CPEE process tree ' +
                    'as an XML document',
                type: 'string',
              })
              .positional('branch2', {
                description: 'Path to the second branch CPEE process tree ' +
                    'as an XML document',
                type: 'string',
              })
              .option('mode', {
                description: 'Select the matching mode to use.',
                alias: 'm',
                type: 'string',
                choices: Object.values(MatchPipeline.MATCH_MODES),
                default: MatchPipeline.MATCH_MODES.QUALITY,
              })
              .option('threshold', {
                description: 'Define the threshold for matching nodes',
                alias: 't',
                type: 'number',
                default: 0.4,
              })
              .option('variablePrefix', {
                description: 'Specify the prefix used to detect read/written ' +
                    'variables in code and arguments',
                alias: 'v',
                type: 'string',
                default: 'data.',
              })
              .option('format', {
                description: 'Select the output format',
                alias: 'f',
                type: 'string',
                choices: [
                  'mergeTree',
                  'std',
                ],
                default: 'std',
              })
              .option('pretty', {
                description: 'Pretty-print the output XML document.',
                alias: 'p',
                type: 'boolean',
                default: false,
              })
              .check((argv) => {
                if (!fs.existsSync(argv.base)) {
                  throw new Error(argv.base + ' ist not a valid file path');
                }
                if (!fs.existsSync(argv.branch1)) {
                  throw new Error(argv.branch1 + ' ist not a valid file path');
                }
                if (!fs.existsSync(argv.branch2)) {
                  throw new Error(argv.branch2 + ' ist not a valid file path');
                }
                return true;
              });
        },
        (argv) => {
          DiffConfig.LOG_LEVEL = argv.logLevel;
          DiffConfig.VARIABLE_PREFIX = argv.variablePrefix;
          DiffConfig.MATCH_MODE = argv.mode;
          DiffConfig.COMPARISON_THRESHOLD = argv.threshold;
          DiffConfig.PRETTY_XML = argv.pretty;
          // Parse
          const parser = new Preprocessor();
          const base = parser.fromFile(argv.base);
          const branch1 = parser.fromFile(argv.branch1);
          const branch2 = parser.fromFile(argv.branch2);

          // Merge
          const merger = new CpeeMerge();
          const merged = merger.merge(base, branch1, branch2);

          Logger.info('Formatting merge result as ' + argv.format);
          switch (argv.format) {
            case 'mergeTree': {
              Logger.result(merged.toXmlString());
              break;
            }
            case 'std': {
              Logger.result(Node.fromNode(merged).toXmlString());
              break;
            }
          }
        },
    )
    .command(
        'patch <old> [editScript]',
        'Patch a document with an edit script',
        (yargs) => {
          yargs
              .positional('old', {
                description: 'Path to the original CPEE process tree as an ' +
                    'XML document',
                type: 'string',
              })
              .positional('editScript', {
                description: 'Path to the edit script as an XML document',
                type: 'string',
              })
              .option('format', {
                description: 'Select the output format',
                alias: 'f',
                type: 'string',
                choices: [
                  'patched',
                  'deltaTree',
                ],
                default: 'patched',
              })
              .option('afterPreprocess', {
                description: 'Show the changes applied during preprocessing.',
                alias: 's',
                type: 'boolean',
                default: false,
              })
              .option('pretty', {
                description: 'Pretty-print the output XML document',
                alias: 'p',
                type: 'boolean',
                default: false,
              })
              .check((argv) => {
                if (!fs.existsSync(argv.old)) {
                  throw new Error(argv.old + ' ist not a valid file path');
                }
                if (argv.editScript != null &&
                    !fs.existsSync(argv.editScript)) {
                  throw new Error(argv.editScript +
                      ' ist not a valid file path');
                }
                return true;
              });
        },
        (argv) => {
          DiffConfig.PRETTY_XML = argv.pretty;
          DiffConfig.LOG_LEVEL = argv.logLevel;
          // Parse
          const parser = new Preprocessor();
          const preProcessorEditScript = new EditScript();
          const oldTree = parser.fromFile(argv.old, preProcessorEditScript);

          if (argv.afterPreprocess) {
            Logger.result('Changes applied during preprocessing:');
            Logger.result(preProcessorEditScript.toXmlString());
          }

          let editScript = new EditScript();
          if (argv.editScript != null) {
            const editScriptContent = fs.readFileSync(argv.editScript)
                .toString();
            editScript = EditScript.fromXmlString(editScriptContent);
          }

          Logger.info('Formatting result as ' + argv.format);
          switch (argv.format) {
            case 'patched': {
              const patcher = new Patcher();
              const patched = patcher.patch(oldTree, editScript);
              Logger.result(patched.toXmlString());
              break;
            }
            case 'deltaTree': {
              const deltaTreeGen = new DeltaTreeGenerator();
              const deltaTree = deltaTreeGen.deltaTree(oldTree, editScript);
              Logger.result(deltaTree.toXmlString());
              break;
            }
          }
        },
    )
    .help()
    .version()
    .demandCommand()
    .strictCommands()
    .argv;