Skip to content

Conversation

@tvolkert
Copy link
Contributor

@tvolkert tvolkert commented Dec 4, 2016

This argument will enable mocking of os-layer process invocations,
where the mock behavior will come from replaying a previously-
recorded set of invocations. At the point of process invocation,
the key metadata for the invocation will be looked up in the
recording's manifest, and iff a matching record exists in the
menifest, the process will be mocked out with data derived from
the corresponding recorded process (e.g. stdout, stderr, ecit code).

This argument will enable mocking of os-layer process invocations,
where the mock behavior will come from replaying a previously-
recorded set of invocations. At the point of process invocation,
the key metadata for the invocation will be looked up in the
recording's manifest, and iff a matching record exists in the
menifest, the process will be mocked out with data derived from
the corresponding recorded process (e.g. stdout, stderr, ecit code).
@tvolkert
Copy link
Contributor Author

tvolkert commented Dec 4, 2016

@tvolkert
Copy link
Contributor Author

tvolkert commented Dec 4, 2016

FYI, again, you need to click "Load diff" to see the full changes in this PR...

Copy link
Contributor

@danrubel danrubel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with comments for you to address as you see fit.

: new File(recordToPath);
}
// Turn on recording.
assert(globalResults['replay-from'] == null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps replace thisassert and the one below with a check earlier in the code that indicates to the user that they can't use both options together...

if (globalResults['record-to'] != null && globalResults['replay-from'] == null)
    throw new ToolExit('Cannot use --record-to and --replay-from options at the same time');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

globalResults['replay-from'].trim(),
));
} on ArgumentError catch (_) {
printError('--replay-from must specify a valid file or directory.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new ToolExit instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

: pid = result.pid,
_stdout = result.stdout,
_stderr = result.stderr,
_stdoutController = new StreamController<List<int>>.broadcast(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are Process stdout/stderr broadcast streams? If not, will there be a race condition or will some stdout/stderr data fall on the floor during replay if these are broadcast streams? I'm not sure of the answer myself...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. No, they're not broadcast - fixed.

// Produce stream events after a delay so that all interested parties have
// a chance to add themselves as listeners before we fire our stream events
// and terminate the process.
new Timer(const Duration(milliseconds: 50), () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah... maybe this delay before adding data to stdout/stderr alleviates the issue I raised above. I prefer not to have arbitrary delays in the code, but this should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this timeout in (for now) and updated the comment to reflect exactly why it's needed. I plan to keep digging to see if I can solve it a better way, but I can do that in a follow-on PR.

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) => delegate.kill(signal);
}

/// a [ProcessManager] implementation that mocks out all process invocations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uber-nit: s/a/A/ here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return new ReplayProcessManager._(manifest, dir);
} on FormatException catch (_) {
throw new ArgumentError('$_kManifestName is not a valid JSON file.');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe assert(false) here for future-proofing in case someone starts throwing something other than FormatException.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline.

Encoding stdoutEncoding,
Encoding stderrEncoding,
}) {
ListsEqual<String> equal = const ListEquality<String>().equals;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make this a private top-level const kListsEqual? If not, make final.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


@override
bool killPid(int pid, [ProcessSignal signal = ProcessSignal.SIGTERM]) {
throw new UnimplementedError();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnimplementedError or UnsupportedError? If Unimplemented, add a TODO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


static Future<dynamic> _getData(String path, String encoding) async {
File file = new File(path);
assert(await file.exists());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd kill the assert here and below. This feels more like a legit case for a runtime exception (e.g., FileSystemException which you'll get from the read calls below) than a logical invariant violation on the part of the programmer. Filesystems can get unmounted at runtime, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@tvolkert
Copy link
Contributor Author

tvolkert commented Dec 5, 2016

Ok, all comments addressed.

Copy link
Contributor

@johnmccutchan johnmccutchan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some comments

/// Recordings are expected to be of the form produced by
/// [RecordingProcessManager]. Namely, this includes:
///
/// - a [_kManifestName](manifest file) encoded as UTF-8 JSON that lists all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc commenting a private member might not work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice we don't actually publish the flutter tools docs anyway. They're for internal use basically. We just use dartdoc format for consistency.

dir = new Directory(location);
break;
case FileSystemEntityType.NOT_FOUND:
throw new ArgumentError.value(location, 'location');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArgumentError takes a message parameter, you could use it to help make this failure case clearer.

// valid replay directory. Namely, we don't validate the structure of the
// JSON within the manifest, and we don't validate the existence of
// all stdout and stderr files referenced in the manifest.
throw new ArgumentError('$_kManifestName not found.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito here.

Map<String, dynamic> entry,
) async {
String basePath = path.join(dir.path, entry['basename']);
return new _ReplayProcessResult._(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure path here where _getData throws an exception (because stdout or stderr can't be found) might bubble up to the callers. Consider catching here and throwing whatever error Process throws when it can't spawn a process.

// prevents the case where our device log reader consumes all stdio before
// we've even started the app, thus causing the protocol discovery process
// to fail.
new Timer(const Duration(milliseconds: 50), () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a TODO here and a comment about this being temporary.

@Hixie
Copy link
Contributor

Hixie commented Dec 5, 2016

Test please.

dependencies:
archive: ^1.0.20
args: ^0.13.4
collection: '>=1.9.1 <2.0.0'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to get out of having this dependency? We've been trying to drop our dependency on package:collection...

argParser.addOption('replay-from',
help:
'Enables mocking of process invocations by replaying their stdout '
', stderr, and exit code from the specified recording (obtained '
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo (stdout , stderr,, extra space before the first comma)

}

@override
Future<Process> start(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please match flutter style for these

}) {
Map<String, dynamic> entry = <String, dynamic>{};
if (pid != null) entry['pid'] = pid;
if (basename != null) entry['basename'] = basename;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change these ifs to have their bodies on a separate line

/// process invocation.
Map<String, dynamic> _createManifestEntry({
int pid,
String basename,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these should be two-character indents not 4

typedef bool ListsEqual<T>(List<T> list1, List<T> list2);

const String _kManifestName = 'MANIFEST.txt';
final ListsEqual<String> kListsEqual = const ListEquality<String>().equals;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than using collection.dart, just have a function somewhere in flutter_tool that does this (if you use it a lot) or just inline it (if not).

if (list1.length != list2.length)
  return false;
for (int index = 0; index < list1.length; index += 1) {
  if (list1[index] != list2[index)
    return false;
}
return true;

@tvolkert
Copy link
Contributor Author

tvolkert commented Dec 6, 2016

All comments have been addressed, modulo adding a test (which will come in a follow-on change). Merging...

@tvolkert tvolkert merged commit 7536404 into flutter:master Dec 6, 2016
@tvolkert tvolkert deleted the replay branch December 6, 2016 18:09
@devoncarew
Copy link
Contributor

@tvolkert, I notice that flutter -h is now printing out args for --record-to and --replay-from. I assume these are less common flags for flutter users to use? If so, it'd suggest hiding them be default, to keep the help text focused on the fewer, more likely options. You can add hide: !verboseHelp as a named param when you define the option.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants