<commit_before>import path from 'path';
import { promises as fsPromises } from 'fs';

import execa from 'execa';
import npmRunPath from 'npm-run-path';
import { StrykerOptions, File } from '@stryker-mutator/api/core';
import { normalizeWhitespaces, I } from '@stryker-mutator/util';
import { Logger } from '@stryker-mutator/api/logging';
import { tokens, commonTokens, Disposable } from '@stryker-mutator/api/plugin';
import { mergeMap, toArray } from 'rxjs/operators';
import { from } from 'rxjs';

import { TemporaryDirectory } from '../utils/temporary-directory';
import { findNodeModulesList, MAX_CONCURRENT_FILE_IO, moveDirectoryRecursiveSync, symlinkJunction, mkdirp } from '../utils/file-utils';
import { coreTokens } from '../di';
import { UnexpectedExitHandler } from '../unexpected-exit-handler';

export class Sandbox implements Disposable {
  private readonly fileMap = new Map<string, string>();
  public readonly workingDirectory: string;
  private readonly backupDirectory: string = '';

  public static readonly inject = tokens(
    commonTokens.options,
    commonTokens.logger,
    coreTokens.temporaryDirectory,
    coreTokens.files,
    coreTokens.execa,
    coreTokens.unexpectedExitRegistry
  );

  constructor(
    private readonly options: StrykerOptions,
    private readonly log: Logger,
    temporaryDirectory: I<TemporaryDirectory>,
    private readonly files: readonly File[],
    private readonly exec: typeof execa,
    unexpectedExitHandler: I<UnexpectedExitHandler>
  ) {
    if (options.inPlace) {
      this.workingDirectory = process.cwd();
      this.backupDirectory = temporaryDirectory.createRandomDirectory('backup');
      this.log.info(
        'In place mode is enabled, Stryker will be overriding YOUR files. Find your backup at: %s',
        path.relative(process.cwd(), this.backupDirectory)
      );
      unexpectedExitHandler.registerHandler(this.dispose.bind(this, true));
    } else {
      this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox');
      this.log.debug('Creating a sandbox for files in %s', this.workingDirectory);
    }
  }

  public async init(): Promise<void> {
    await this.fillSandbox();
    await this.runBuildCommand();
    await this.symlinkNodeModulesIfNeeded();
  }

  public sandboxFileFor(fileName: string): string {
    const sandboxFileName = this.fileMap.get(fileName);
    if (sandboxFileName === undefined) {
      throw new Error(`Cannot find sandbox file for ${fileName}`);
    }
    return sandboxFileName;
  }

  private fillSandbox(): Promise<void[]> {
    return from(this.files)
      .pipe(
        mergeMap((file) => this.fillFile(file), MAX_CONCURRENT_FILE_IO),
        toArray()
      )
      .toPromise();
  }

  private async runBuildCommand() {
    if (this.options.buildCommand) {
      const env = npmRunPath.env();
      this.log.info('Running build command "%s" in "%s".', this.options.buildCommand, this.workingDirectory);
      this.log.debug('(using PATH: %s)', env.PATH);
      await this.exec.command(this.options.buildCommand, { cwd: this.workingDirectory, env });
    }
  }

  private async symlinkNodeModulesIfNeeded(): Promise<void> {
    this.log.info('Start symlink node_modules');
    if (this.options.symlinkNodeModules && !this.options.inPlace) {
      // TODO: Change with this.options.basePath when we have it
      const basePath = process.cwd();
      const nodeModulesList = await findNodeModulesList(basePath, this.options.tempDirName);

      if (nodeModulesList.length > 0) {
        for (const nodeModules of nodeModulesList) {
          this.log.debug(`Create symlink from ${path.resolve(nodeModules)} to ${path.join(this.workingDirectory, nodeModules)}`);
          await symlinkJunction(path.resolve(nodeModules), path.join(this.workingDirectory, nodeModules)).catch((error: NodeJS.ErrnoException) => {<commit_msg>fix(logging): log info about symlinking on debug (#2756)<commit_after>import path from 'path';
import { promises as fsPromises } from 'fs';

import execa from 'execa';
import npmRunPath from 'npm-run-path';
import { StrykerOptions, File } from '@stryker-mutator/api/core';
import { normalizeWhitespaces, I } from '@stryker-mutator/util';
import { Logger } from '@stryker-mutator/api/logging';
import { tokens, commonTokens, Disposable } from '@stryker-mutator/api/plugin';
import { mergeMap, toArray } from 'rxjs/operators';
import { from } from 'rxjs';

import { TemporaryDirectory } from '../utils/temporary-directory';
import { findNodeModulesList, MAX_CONCURRENT_FILE_IO, moveDirectoryRecursiveSync, symlinkJunction, mkdirp } from '../utils/file-utils';
import { coreTokens } from '../di';
import { UnexpectedExitHandler } from '../unexpected-exit-handler';

export class Sandbox implements Disposable {
  private readonly fileMap = new Map<string, string>();
  public readonly workingDirectory: string;
  private readonly backupDirectory: string = '';

  public static readonly inject = tokens(
    commonTokens.options,
    commonTokens.logger,
    coreTokens.temporaryDirectory,
    coreTokens.files,
    coreTokens.execa,
    coreTokens.unexpectedExitRegistry
  );

  constructor(
    private readonly options: StrykerOptions,
    private readonly log: Logger,
    temporaryDirectory: I<TemporaryDirectory>,
    private readonly files: readonly File[],
    private readonly exec: typeof execa,
    unexpectedExitHandler: I<UnexpectedExitHandler>
  ) {
    if (options.inPlace) {
      this.workingDirectory = process.cwd();
      this.backupDirectory = temporaryDirectory.createRandomDirectory('backup');
      this.log.info(
        'In place mode is enabled, Stryker will be overriding YOUR files. Find your backup at: %s',
        path.relative(process.cwd(), this.backupDirectory)
      );
      unexpectedExitHandler.registerHandler(this.dispose.bind(this, true));
    } else {
      this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox');
      this.log.debug('Creating a sandbox for files in %s', this.workingDirectory);
    }
  }

  public async init(): Promise<void> {
    await this.fillSandbox();
    await this.runBuildCommand();
    await this.symlinkNodeModulesIfNeeded();
  }

  public sandboxFileFor(fileName: string): string {
    const sandboxFileName = this.fileMap.get(fileName);
    if (sandboxFileName === undefined) {
      throw new Error(`Cannot find sandbox file for ${fileName}`);
    }
    return sandboxFileName;
  }

  private fillSandbox(): Promise<void[]> {
    return from(this.files)
      .pipe(
        mergeMap((file) => this.fillFile(file), MAX_CONCURRENT_FILE_IO),
        toArray()
      )
      .toPromise();
  }

  private async runBuildCommand() {
    if (this.options.buildCommand) {
      const env = npmRunPath.env();
      this.log.info('Running build command "%s" in "%s".', this.options.buildCommand, this.workingDirectory);
      this.log.debug('(using PATH: %s)', env.PATH);
      await this.exec.command(this.options.buildCommand, { cwd: this.workingDirectory, env });
    }
  }

  private async symlinkNodeModulesIfNeeded(): Promise<void> {
    this.log.debug('Start symlink node_modules');
    if (this.options.symlinkNodeModules && !this.options.inPlace) {
      // TODO: Change with this.options.basePath when we have it
      const basePath = process.cwd();
      const nodeModulesList = await findNodeModulesList(basePath, this.options.tempDirName);

      if (nodeModulesList.length > 0) {
        for (const nodeModules of nodeModulesList) {
          this.log.debug(`Create symlink from ${path.resolve(nodeModules)} to ${path.join(this.workingDirectory, nodeModules)}`);
          await symlinkJunction(path.resolve(nodeModules), path.join(this.workingDirectory, nodeModules)).catch((error: NodeJS.ErrnoException) => {
{'fraction_non_alphanumeric': 0.1188883170355121, 'fraction_numerical': 0.0007720020586721565, 'mean_word_length': 4.008376288659794, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6943097', 'n_tokens_mistral': 2237, 'n_tokens_neox': 2264, 'n_words': 547}