content: package com.google.javascript.clutz;
  
  import static com.google.common.truth.Truth.assertThat;
  import static com.google.common.truth.Truth.assert_;
  import static java.nio.charset.StandardCharsets.UTF_8;
  import static java.util.Collections.singletonList;
  
  import com.google.common.base.Charsets;
  import com.google.common.base.Joiner;
  import com.google.common.collect.ImmutableSet;
  import com.google.common.collect.Lists;
  import com.google.common.io.Files;
  import com.google.common.truth.FailureMetadata;
  import com.google.common.truth.StringSubject;
  import com.google.common.truth.Subject;
  import com.google.javascript.jscomp.SourceFile;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.IOException;
  import java.io.PrintStream;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.regex.Pattern;
  import javax.annotation.Nullable;
  
  /** A subject that supports assertions on {@link DeclarationGenerator}'s results. */
  class ProgramSubject extends Subject {
  
    /**
     * A global root for all file access for testing in clutz. Intentionally left empty here, so that
     * some forks (like google's internal environment) can overwrite it with a non-empty value.
     */
    static final String SOURCE_ROOT = "";
  
    static String resource(String path) {
      return SOURCE_ROOT + path;
    }
  
    /**
     * A stripped down version of Closure's base.js for Clutz tests. In real total clutz runs we
     * always pass the real closure's base.js.
     */
    private static final SourceFile CLUTZ_GOOG_BASE_TOTAL =
        SourceFile.fromFile(
            resource("src/test/java/com/google/javascript/clutz/testdata/base.js"), UTF_8);
  
    /**
     * A even more stripped down version of Closure's base.js for incremental Clutz tests. In
     * production we also use this file for every incremental run, because the original base.js is not
     * passed in.
     */
    private static final SourceFile CLUTZ_GOOG_BASE_INCR =
        SourceFile.fromFile(resource("src/resources/partial_goog_base.js"), UTF_8);
  
    private final ProgramSubject.Program actual;
    public boolean withPlatform = false;
    public boolean partialInput = false;
    public String extraExternFile = null;
    public boolean emitBase = false;
    public String depgraph = null;
    public boolean debug = true;
  
    static ProgramSubject assertThatProgram(String... sourceLines) {
      String sourceText = Joiner.on('\n').join(sourceLines);
      return assert_().about(ProgramSubject.FACTORY).that(new Program(sourceText));
    }
  
    static ProgramSubject assertThatProgram(File singleInput) {
      return assertThatProgram(singletonList(singleInput), Collections.<File>emptyList());
    }
  
    static ProgramSubject assertThatProgram(List<File> roots, List<File> nonroots) {
      Program program = new Program(roots, nonroots);
      return assert_().about(ProgramSubject.FACTORY).that(program);
    }
  
    static final Subject.Factory<ProgramSubject, ProgramSubject.Program> FACTORY =
        new Subject.Factory<ProgramSubject, ProgramSubject.Program>() {
          @Override
          public ProgramSubject createSubject(
              FailureMetadata failureMetadata, ProgramSubject.Program that) {
            return new ProgramSubject(failureMetadata, that);
          }
        };
  
    ProgramSubject(FailureMetadata failureMetadata, ProgramSubject.Program subject) {
      super(failureMetadata, subject);
      this.actual = subject;
    }
  
    void generatesDeclarations(File golden) throws IOException {
      String[] parseResult = parse();
      assertThat(parseResult[1]).isEqualTo("");
      String actual = parseResult[0];
      String stripped =
          DeclarationGeneratorTest.GOLDEN_FILE_COMMENTS_REGEXP.matcher(actual).replaceAll("");
      String expected = DeclarationGeneratorTest.getTestFileText(golden);
      String expectedClean =
          DeclarationGeneratorTest.GOLDEN_FILE_COMMENTS_REGEXP.matcher(expected).replaceAll("");
      if (!stripped.equals(expectedClean)) {
        // If the `UPDATE_GOLDENS` flag is set, overwrite the golden files, unless it's a `_with_platform.d.ts`
        // which have 2 golden files that are concatenated, or if the golden has comments, both of which
        // shouldn't be blindly overwritten
        if (System.getenv("UPDATE_GOLDENS") != null
            && !golden.getName().endsWith("_with_platform.d.ts")
            && expected.equals(expectedClean)) {
          Files.asCharSink(golden, Charsets.UTF_8).write(stripped);
        } else {
          check("generatedDeclarations()")
              .withMessage("Verifying %s", golden.getName())
              .that(stripped)
              .isEqualTo(expected);
        }
      }
    }
  
    StringSubject diagnosticStream() {
      String[] parseResult = parse();
      return assertThat(parseResult[1]);
    }
  
    private String[] parse() throws AssertionError {
      Options opts = new Options();
      opts.debug = debug;
      opts.skipEmitPattern = Pattern.compile(".*\\.skip\\.tsickle\\.js$");
      if (partialInput) {
        opts.partialInput = true;
      }
      opts.collidingProvides = ImmutableSet.of("colliding_provide.aliased");
      opts.browserResolverStrippedPrefixes = Arrays.asList("abs_strip_for_testing");
  
      List<SourceFile> sourceFiles = new ArrayList<>();
  
      // base.js is needed for the type declaration of goog.require for
      // all total tests, except the base.js one itself.
      if (actual.roots.isEmpty()) {
        sourceFiles.add(CLUTZ_GOOG_BASE_TOTAL);
      } else {
        File firstFile = actual.roots.get(0);
        if (firstFile.getParentFile().getName().equals("partial")) {
          sourceFiles.add(CLUTZ_GOOG_BASE_INCR);
        } else if (!firstFile.getName().equals("base.js")) {
          sourceFiles.add(CLUTZ_GOOG_BASE_TOTAL);
        }
      }
  
      Set<String> nonroots = new LinkedHashSet<>();
      for (File nonroot : actual.nonroots) {
        sourceFiles.add(SourceFile.fromPath(nonroot.toPath(), UTF_8));
        nonroots.add(nonroot.getPath().replace(System.getProperty("file.separator"), "/"));
      }
  
      Set<String> roots = new LinkedHashSet<>();
  
      for (File root : actual.roots) {
        sourceFiles.add(SourceFile.fromPath(root.toPath(), UTF_8));
        roots.add(root.getPath().replace(System.getProperty("file.separator"), "/"));
      }
  
      if (actual.sourceText != null) {
        sourceFiles.add(SourceFile.fromCode("main.js", actual.sourceText));
        roots.add("main.js");
      }
  
      if (emitBase) {
        roots.add(CLUTZ_GOOG_BASE_TOTAL.getName());
      }
  
      List<SourceFile> externFiles;
      if (withPlatform) {
        externFiles = DeclarationGenerator.getDefaultExterns(opts);
      } else {
        // Clutz is very permissive in its inputs, thus supporting ES6 code. Closure refuses
        // to compile ES6 unless es6.js extern is passed in. To speed up test exectution we
        // pass a thin shim instead of the real es6.js extern.
        externFiles =
            Lists.newArrayList(SourceFile.fromFile(resource("src/resources/es6_min.js"), UTF_8));
      }
  
      if (depgraph != null) {
        opts.depgraph = DepgraphTest.parseFile(depgraph);
      } else {
        opts.depgraph = Depgraph.forRoots(roots, nonroots);
      }
  
      if (extraExternFile != null) {
        externFiles.add(SourceFile.fromFile(extraExternFile, UTF_8));
      }
  
      PrintStream err = System.err;
      try {
        // Admittedly the PrintStream setting is a bit hacky, but it's closest to how users would
        // run Clutz.
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        System.setErr(new PrintStream(out));
        DeclarationGenerator generator = new DeclarationGenerator(opts);
        String dts =
            generator.generateDeclarations(
                sourceFiles, externFiles, Depgraph.forRoots(roots, nonroots));
        String diagnostics = out.toString();
        return new String[] {dts, diagnostics};
      } finally {
        System.setErr(err);
      }
    }
  
    static class Program {
      private final List<File> roots;
      private final List<File> nonroots;
      @Nullable private final String sourceText;
  
      Program(String sourceText) {
        this.roots = Collections.emptyList();
        this.nonroots = Collections.emptyList();
        this.sourceText = sourceText;
      }
  
      Program(List<File> roots, List<File> nonroots) {
        this.roots = roots;
        this.nonroots = nonroots;
        this.sourceText = null;
      }
    }
  }
  