-
Notifications
You must be signed in to change notification settings - Fork 29.6k
Add --replay-from argument to Flutter tools #7146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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).
|
FYI, again, you need to click "Load diff" to see the full changes in this PR... |
danrubel
left a comment
There was a problem hiding this 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); |
There was a problem hiding this comment.
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');
There was a problem hiding this comment.
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.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw new ToolExit instead?
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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), () { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
cbracken
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) => delegate.kill(signal); | ||
| } | ||
|
|
||
| /// a [ProcessManager] implementation that mocks out all process invocations |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.'); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
Ok, all comments addressed. |
johnmccutchan
left a comment
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.'); |
There was a problem hiding this comment.
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._( |
There was a problem hiding this comment.
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), () { |
There was a problem hiding this comment.
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.
|
Test please. |
packages/flutter_tools/pubspec.yaml
Outdated
| dependencies: | ||
| archive: ^1.0.20 | ||
| args: ^0.13.4 | ||
| collection: '>=1.9.1 <2.0.0' |
There was a problem hiding this comment.
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 ' |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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;|
All comments have been addressed, modulo adding a test (which will come in a follow-on change). Merging... |
|
@tvolkert, I notice that |

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).