test icon indicating copy to clipboard operation
test copied to clipboard

Add the ability to get the current script or project path when using the new test runner

Open alextekartik opened this issue 9 years ago • 38 comments

http://stackoverflow.com/questions/30214563/get-current-script-path-or-current-project-path-using-new-test-runner I am porting old vm unittest files using the new test package. Some relies on input files in sub directories of my test folder. Before I was using Platform.script to find the location of such files. This works fine when using

$ dart test/my_test.dart

However using

$ pub run test

this is now pointing to a temp folder (tmp/dart_test_xxxx/runInIsolate.dart). I am unable to locate my test input files anymore. I cannot rely on the current path as I might run the test from a different working directory.

Is there a way to find the location of my_test.dart (or event the project root path), from which I could derive the locations of my files?

alextekartik avatar May 13 '15 13:05 alextekartik

This is the sort of thing that should be exposed through the API (#48).

nex3 avatar May 13 '15 18:05 nex3

/sub ... I'm having the same problem (also covered in #123). Our existing tests use Platform.script to load data, making hard to migrate to test runner.

Anecdotally: most packages I've worked on with VM tests end up using data files.

Anyway, I'm guessing I can work around this with dart:mirrors ... instead of Platform.script, find the script's library URI via mirrors.

jmesserly avatar Jun 01 '15 20:06 jmesserly

Is there a simple hack/workaround for this, that we can use until it is implemented? (Apart from setting an environment variable)

enyo avatar Jun 02 '15 15:06 enyo

I use the following ugly workaround (cross posted on SO) that gets me the directory name of the current test script if I'm running it directly or with pub run test. It will definitely break if anything in the implementation changes but I needed this desperately...

library test_utils.test_script_dir;

import 'dart:io';
import 'package:path/path.dart';

// temp workaround using test package
String get testScriptDir {
  String scriptFilePath = Platform.script.toFilePath();
  print(scriptFilePath);
  if (scriptFilePath.endsWith("runInIsolate.dart")) {

    // Let's look for this line:
    // import "file:///path_to_my_test/test_test.dart" as test;

    String importLineBegin = 'import "file://';
    String importLineEnd = '" as test;';
    int importLineBeginLength = importLineBegin.length;

    String scriptContent = new File.fromUri(Platform.script).readAsStringSync();

    int beginIndex = scriptContent.indexOf(importLineBegin);
    if (beginIndex > -1) {
      int endIndex = scriptContent.indexOf(importLineEnd, beginIndex + importLineBeginLength);
      if (endIndex > -1) {
        scriptFilePath = scriptContent.substring(beginIndex + importLineBegin.length, endIndex);
      }
    }
  }
  return dirname(scriptFilePath);
}

alextekartik avatar Jun 02 '15 16:06 alextekartik

fyi, I got it working with something a bit simpler: https://github.com/dart-lang/dev_compiler/blob/master/test/test_util.dart

import 'dart:mirrors';
import 'package:path/path.dart' as path;

final String testDirectory =
    path.dirname((reflectClass(_TestUtils).owner as LibraryMirror).uri.path);

class _TestUtils {}

Basically, get your own library through mirrors and grab the URI path.

jmesserly avatar Jun 02 '15 18:06 jmesserly

If you don't want to make a fake class, you can also do:

currentMirrorSystem().findLibrary(#your_library).uri.path);

munificent avatar Jun 19 '15 19:06 munificent

Is this a bug/feature of pub run? Should there be a sister issue over at dart-lang/pub?

srawlins avatar Oct 27 '15 21:10 srawlins

No, if anything, it's an issue with the core libraries. The forthcoming resource/package-spec stuff should solve it, though.

nex3 avatar Oct 27 '15 22:10 nex3

@nex3: Regarding your last comment, do you know if it's been solved yet? Or if there is an SDK issue to link to? I'd love an issue to track.

srawlins avatar May 18 '17 21:05 srawlins

I had this problem when I trying read some files in my tests section, to solve that and the problem to open file em multiple platforms, I write the following script:

String retrieveFilePath(String fileName, [String baseDir]){
  var context;
  // get platform context
  if(Platform.isWindows) {
    context = path.Context(style:path.Style.windows);
  } else {
    context = path.Context(style:path.Style.posix);
  }

  // case baseDir not informed, get current script dir
  baseDir ??= path.dirname(Platform.script.path);
  // join dirPath with fileName
  var filePath = context.join(baseDir, fileName);
  // convert Uri to String to make the string treatment more easy
  filePath = context.fromUri(context.normalize(filePath));
  // remove possibles extra paths generated by testing routines
  filePath = path.fromUri(filePath).split('file:').last;

  return filePath;
}

polotto avatar Jul 26 '19 19:07 polotto

I think this issue should be taken up again. Seems like the Resource class didn't happen - and dart:mirrors is more or less deprecated.

It would be really nice to have a getter in package:test for the current test script location or some other reliable and intuitive way of getting at the path.

sigurdm avatar Oct 07 '21 13:10 sigurdm

@sigurdm directory like test/data/ or something similar?

One concern with exposing a path only is that it isn't useful on the web. If we make APIs for reading files, we could potentially make it work on all platforms and build systems. Differences in the directory layout is an ongoing pain point between dart test and bazel test and a specific file reading API could work around that.

natebosch avatar Oct 07 '21 18:10 natebosch

Yeah - that would work for me!

sigurdm avatar Oct 08 '21 07:10 sigurdm

test/data/ is exactly what I want to use this for aswell

Jonas-Sander avatar Nov 04 '21 20:11 Jonas-Sander

Capturing some thoughts about this after some conversations with @jakemac53 and @nshahan

An alternative could be a builder (eventually a macro) that inlines file content to a const String or list of bytes in Dart source. Even if we had macros today, this alternative might not be satisfying since it doesn't allow for reading a large file, handling it, and then freeing it from memory before reading the next.

I had proposed a hardcoded directory test/data/ for this, but paths could also be relative to:

  • The package root
  • The currently running test suite.
  • The file that makes the call.

I think we prefer either a hardcoded directory or the package root. Relative to the file that makes this too complicated to implement, and relative to the currently running test suite could be confusing.

Use the package root: Pros

  • Feels intuitive to Dart users, not magical. Matches CWD for external test runner usage, so easy transition to make.
  • Maximally flexible, can read pubspec.yaml which is a common use case to check during tests.
  • Can lay out data and tests in the same directory instead of spreading them across trees.
  • The "package root" concept is more fully baked and consistent than it used to be. It used to be only a convention, but now it's also encoded in the package config.

Cons

  • Need to make decisions about how to limit what is read. Do we allow reading .dart_tool/ or do we need to try to block some things?
  • May need more implementation effort to normalize across environment. If we don't limit what files can be read, then either we need to take care that bazel exposes the same files (possibly through a multi directory overlay if outputs are spread out) or accept that there are still differences in behavior.
  • Cannot automatically include the conventional directory as data in bazel, or as a required output with build_runner test.

Use test/data/: Pros

  • No need to limit what is read, anything in this directory can is assumed intended to be readable.
  • Easy to implement consistently in all build systems.
  • Can automatically include as data in bazel, and as an output in build_runner test.
  • Convention makes it easier to jump between packages and know where to look.

Cons

  • Some existing test data would need to move.
  • Can't colocate test files and data files.
  • Can't read pubspec.yaml.
  • More magic (although a name like readTestData('foo.txt') is easy to associate with test/data/foo.txt)

natebosch avatar Apr 12 '22 20:04 natebosch

An example of the type of code written to work around the environment differences:

https://dart-review.googlesource.com/c/sdk/+/242162

Neither of the proposals above would solve this issue. Either could solve the issue if there was a step outside of running the tests which copied or symlinked the files into the package somewhere. I could imagine doing that in bazel in place of setting up the environment variables, but we wouldn't have a good solution for this case externally.

natebosch avatar Apr 28 '22 17:04 natebosch

There is some potential for aligning this with our internal bazel build rules and having a close coupling with the data argument to the test target.

b/235365551

natebosch avatar Jun 08 '22 18:06 natebosch

Yes, I think we should read from $RUNFILES/<bazel-package-root> which would contain anything given in data. These aren't compile time deps but runtime deps.

jakemac53 avatar Jun 08 '22 19:06 jakemac53

Another thing to consider here is reading data from other packages, likely via package: uris. This should also work in bazel but you will have to set up appropriate dependencies on the targets that expose those files, I believe that could just be file groups which you depend on in your data dependencies as well.

jakemac53 avatar Jun 08 '22 19:06 jakemac53

Another hacky alternative is to extract the path from the stack trace. I think this works (for non-web cases)?

import 'package:stack_trace/stack_trace.dart' as stacktrace;

String currentDartFilePath({bool packageRelative = false}) {
  var caller = stacktrace.Frame.caller(1);
  return packageRelative ? caller.library : caller.uri.toFilePath();
}

jamesderlin avatar Feb 26 '23 11:02 jamesderlin

It's very plausible that things like parsing the stack trace will work in some environments and not others. I don't know the specific behavior in bazel internally.

natebosch avatar Mar 02 '23 21:03 natebosch

I also ran into this, and also made a workaround.

Without knowing about this discussion, I also gravitated towards using the package root.

That being said, a test/data/ approach would have probably also worked.

FWIW, my code (only tested on VM, not in browser, and not tested with Bazel).

/// Test files are run in a variety of ways, find this package root in all.
///
/// Test files can be run from source from any working directory. The Dart SDK
/// `tools/test.py` runs them from the root of the SDK for example.
///
/// Test files can be run from dill from the root of package. `package:test`
/// does this.
Uri findPackageRoot(String packageName) {
  final script = Platform.script;
  final fileName = script.name;
  if (fileName.endsWith('_test.dart')) {
    // We're likely running from source.
    var directory = script.resolve('.');
    while (true) {
      final dirName = directory.name;
      if (dirName == packageName) {
        return directory;
      }
      final parent = directory.resolve('..');
      if (parent == directory) break;
      directory = parent;
    }
  } else if (fileName.endsWith('.dill')) {
    final cwd = Directory.current.uri;
    final dirName = cwd.name;
    if (dirName == packageName) {
      return cwd;
    }
  }
  throw StateError("Could not find package root for package '$packageName'. "
      'Tried finding the package root via Platform.script '
      "'${Platform.script.toFilePath()}' and Directory.current "
      "'${Directory.current.uri.toFilePath()}'.");
}

Uri packageUri = findPackageRoot('c_compiler');

extension on Uri {
  String get name => pathSegments.where((e) => e != '').last;
}

dcharkes avatar Apr 14 '23 10:04 dcharkes

Fwiw, to get the root of the current package it might be a bit more robust to use package:package_config and Isolate.packageConfigUri, and then you can just look up the root of your package by name (assuming you know the name of the package, this can't be easily fully generalized). This will also only work on the VM though.

jakemac53 avatar Apr 14 '23 14:04 jakemac53

That being said, a test/data/ approach would have probably also worked.

My test data contains dart projects that contain tests, so nesting it inside test/ doesn't work for my use case. Instead, I'm now using test_data/. (If we had a way to skip test files inside test/data/ when doing dart test but not skip them when doing cd test/data/foo_project/ && dart test that would also work for my use case.)

dcharkes avatar Jun 26 '23 18:06 dcharkes

We do have the filename configuration option https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#filename. You should be able to use that in the top level package to include only the dirs you want, and then assuming that test/data/foo_project is itself a package, dart test under there should still run all the tests.

jakemac53 avatar Jun 26 '23 19:06 jakemac53

We do have the filename configuration option https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#filename. You should be able to use that in the top level package to include only the dirs you want, and then assuming that test/data/foo_project is itself a package, dart test under there should still run all the tests.

Don't you mean paths? The documentation for filename says it only takes into account the base-name, not the full path (and separators).

The paths are not globs, so we have to enumerate everything under test/ that is not test/data/. That works: https://github.com/dart-lang/native/pull/69.

dcharkes avatar Jun 26 '23 20:06 dcharkes

The documentation for filename says it only takes into account the base-name, not the full path (and separators).

Ah I didn't catch that it was basename only. Using paths should also work although what you really want is like an exclude_paths.

jakemac53 avatar Jun 26 '23 20:06 jakemac53

Can this be part of the programmatic API, that is, a pkg/test(_api|core):

/// Returns the package root (the directory with `pubspec.yaml`) for the currently running test.
///
/// This is a partial replacement for the functionality of `Platform.script.path`, but works regardless of whether
/// the script is run directly (`dart run test/my_test.dart`) or through the test runner (`dart test test/my_test.dart`).
String get currentPackageRoot {
  // ...
}

I would find this super helpful, and it would make adopting something like dart test --workspace possible.

matanlurey avatar Sep 10 '24 19:09 matanlurey

What would the expected behavior be for non-cli tests? For example web tests or flutter tests.

Or even for cli tests, it gets weird in bazel or build_runner test

jakemac53 avatar Sep 10 '24 19:09 jakemac53

Can this be part of the programmatic API, that is, a pkg/test(_api|core):

We'd prefer it not be, since this isn't a path we can provide in google3.

What would you do with that information after you have it? And would you be skipping those tests in google3?

natebosch avatar Sep 10 '24 19:09 natebosch