OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 import 'dart:async'; |
| 6 // We need to use the 'io' prefix here, otherwise io.exitCode will shadow |
| 7 // CommandOutput.exitCode in subclasses of CommandOutput. |
| 8 import 'dart:io' as io; |
| 9 |
| 10 import 'command_output.dart'; |
| 11 import 'configuration.dart'; |
| 12 import 'expectation.dart'; |
| 13 import 'path.dart'; |
| 14 import 'utils.dart'; |
| 15 |
| 16 /// A command executed as a step in a test case. |
| 17 class Command { |
| 18 static Command contentShell( |
| 19 String executable, |
| 20 String htmlFile, |
| 21 List<String> options, |
| 22 List<String> dartFlags, |
| 23 Map<String, String> environment) { |
| 24 return new ContentShellCommand._( |
| 25 executable, htmlFile, options, dartFlags, environment); |
| 26 } |
| 27 |
| 28 static Command browserTest(String url, Configuration configuration, |
| 29 {bool retry}) { |
| 30 return new BrowserTestCommand._(url, configuration, retry); |
| 31 } |
| 32 |
| 33 static Command browserHtmlTest( |
| 34 String url, Configuration configuration, List<String> expectedMessages, |
| 35 {bool retry}) { |
| 36 return new BrowserHtmlTestCommand._( |
| 37 url, configuration, expectedMessages, retry); |
| 38 } |
| 39 |
| 40 static Command compilation( |
| 41 String displayName, |
| 42 String outputFile, |
| 43 bool neverSkipCompilation, |
| 44 List<Uri> bootstrapDependencies, |
| 45 String executable, |
| 46 List<String> arguments, |
| 47 Map<String, String> environment) { |
| 48 return new CompilationCommand._( |
| 49 displayName, |
| 50 outputFile, |
| 51 neverSkipCompilation, |
| 52 bootstrapDependencies, |
| 53 executable, |
| 54 arguments, |
| 55 environment); |
| 56 } |
| 57 |
| 58 static Command kernelCompilation( |
| 59 String outputFile, |
| 60 bool neverSkipCompilation, |
| 61 List<Uri> bootstrapDependencies, |
| 62 String executable, |
| 63 List<String> arguments, |
| 64 Map<String, String> environment) { |
| 65 return new KernelCompilationCommand._(outputFile, neverSkipCompilation, |
| 66 bootstrapDependencies, executable, arguments, environment); |
| 67 } |
| 68 |
| 69 static Command analysis(String executable, List<String> arguments, |
| 70 Map<String, String> environmentOverrides) { |
| 71 return new AnalysisCommand._(executable, arguments, environmentOverrides); |
| 72 } |
| 73 |
| 74 static Command vm(String executable, List<String> arguments, |
| 75 Map<String, String> environmentOverrides) { |
| 76 return new VmCommand._(executable, arguments, environmentOverrides); |
| 77 } |
| 78 |
| 79 static Command vmBatch(String executable, String tester, |
| 80 List<String> arguments, Map<String, String> environmentOverrides, |
| 81 {bool checked: true}) { |
| 82 return new VmBatchCommand._( |
| 83 executable, tester, arguments, environmentOverrides, |
| 84 checked: checked); |
| 85 } |
| 86 |
| 87 static Command adbPrecompiled(String precompiledRunner, String processTest, |
| 88 String testDirectory, List<String> arguments, bool useBlobs) { |
| 89 return new AdbPrecompilationCommand._( |
| 90 precompiledRunner, processTest, testDirectory, arguments, useBlobs); |
| 91 } |
| 92 |
| 93 static Command jsCommandLine( |
| 94 String displayName, String executable, List<String> arguments, |
| 95 [Map<String, String> environment]) { |
| 96 return new JSCommandlineCommand._( |
| 97 displayName, executable, arguments, environment); |
| 98 } |
| 99 |
| 100 static Command process( |
| 101 String displayName, String executable, List<String> arguments, |
| 102 [Map<String, String> environment, String workingDirectory]) { |
| 103 return new ProcessCommand._( |
| 104 displayName, executable, arguments, environment, workingDirectory); |
| 105 } |
| 106 |
| 107 static Command copy(String sourceDirectory, String destinationDirectory) { |
| 108 return new CleanDirectoryCopyCommand._( |
| 109 sourceDirectory, destinationDirectory); |
| 110 } |
| 111 |
| 112 static Command pub(String pubCommand, String pubExecutable, |
| 113 String pubspecYamlDirectory, String pubCacheDirectory, |
| 114 {List<String> arguments: const <String>[]}) { |
| 115 return new PubCommand._(pubCommand, pubExecutable, pubspecYamlDirectory, |
| 116 pubCacheDirectory, arguments); |
| 117 } |
| 118 |
| 119 static Command makeSymlink(String link, String target) { |
| 120 return new MakeSymlinkCommand._(link, target); |
| 121 } |
| 122 |
| 123 /// A descriptive name for this command. |
| 124 final String displayName; |
| 125 |
| 126 /// Number of times this command *can* be retried. |
| 127 int get maxNumRetries => 2; |
| 128 |
| 129 /// Reproduction command. |
| 130 String get reproductionCommand => null; |
| 131 |
| 132 /// We compute the Command.hashCode lazily and cache it here, since it might |
| 133 /// be expensive to compute (and hashCode is called often). |
| 134 int _cachedHashCode; |
| 135 |
| 136 Command._(this.displayName); |
| 137 |
| 138 int get hashCode { |
| 139 if (_cachedHashCode == null) { |
| 140 var builder = new HashCodeBuilder(); |
| 141 _buildHashCode(builder); |
| 142 _cachedHashCode = builder.value; |
| 143 } |
| 144 return _cachedHashCode; |
| 145 } |
| 146 |
| 147 operator ==(Object other) => |
| 148 identical(this, other) || |
| 149 (runtimeType == other.runtimeType && _equal(other as Command)); |
| 150 |
| 151 void _buildHashCode(HashCodeBuilder builder) { |
| 152 builder.addJson(displayName); |
| 153 } |
| 154 |
| 155 bool _equal(covariant Command other) => |
| 156 hashCode == other.hashCode && displayName == other.displayName; |
| 157 |
| 158 String toString() => reproductionCommand; |
| 159 |
| 160 Future<bool> get outputIsUpToDate => new Future.value(false); |
| 161 } |
| 162 |
| 163 class ProcessCommand extends Command { |
| 164 /// Path to the executable of this command. |
| 165 String executable; |
| 166 |
| 167 /// Command line arguments to the executable. |
| 168 final List<String> arguments; |
| 169 |
| 170 /// Environment for the command. |
| 171 final Map<String, String> environmentOverrides; |
| 172 |
| 173 /// Working directory for the command. |
| 174 final String workingDirectory; |
| 175 |
| 176 ProcessCommand._(String displayName, this.executable, this.arguments, |
| 177 [this.environmentOverrides, this.workingDirectory]) |
| 178 : super._(displayName) { |
| 179 if (io.Platform.operatingSystem == 'windows') { |
| 180 // Windows can't handle the first command if it is a .bat file or the like |
| 181 // with the slashes going the other direction. |
| 182 // NOTE: Issue 1306 |
| 183 executable = executable.replaceAll('/', '\\'); |
| 184 } |
| 185 } |
| 186 |
| 187 void _buildHashCode(HashCodeBuilder builder) { |
| 188 super._buildHashCode(builder); |
| 189 builder.addJson(executable); |
| 190 builder.addJson(workingDirectory); |
| 191 builder.addJson(arguments); |
| 192 builder.addJson(environmentOverrides); |
| 193 } |
| 194 |
| 195 bool _equal(ProcessCommand other) => |
| 196 super._equal(other) && |
| 197 executable == other.executable && |
| 198 deepJsonCompare(arguments, other.arguments) && |
| 199 workingDirectory == other.workingDirectory && |
| 200 deepJsonCompare(environmentOverrides, other.environmentOverrides); |
| 201 |
| 202 String get reproductionCommand { |
| 203 var env = new StringBuffer(); |
| 204 environmentOverrides?.forEach((key, value) => |
| 205 (io.Platform.operatingSystem == 'windows') |
| 206 ? env.write('set $key=${escapeCommandLineArgument(value)} & ') |
| 207 : env.write('$key=${escapeCommandLineArgument(value)} ')); |
| 208 var command = ([executable]..addAll(batchArguments)..addAll(arguments)) |
| 209 .map(escapeCommandLineArgument) |
| 210 .join(' '); |
| 211 if (workingDirectory != null) { |
| 212 command = "$command (working directory: $workingDirectory)"; |
| 213 } |
| 214 return "$env$command"; |
| 215 } |
| 216 |
| 217 Future<bool> get outputIsUpToDate => new Future.value(false); |
| 218 |
| 219 /// Arguments that are passed to the process when starting batch mode. |
| 220 /// |
| 221 /// In non-batch mode, they should be passed before [arguments]. |
| 222 List<String> get batchArguments => const []; |
| 223 } |
| 224 |
| 225 class CompilationCommand extends ProcessCommand { |
| 226 final String _outputFile; |
| 227 final bool _neverSkipCompilation; |
| 228 final List<Uri> _bootstrapDependencies; |
| 229 |
| 230 CompilationCommand._( |
| 231 String displayName, |
| 232 this._outputFile, |
| 233 this._neverSkipCompilation, |
| 234 this._bootstrapDependencies, |
| 235 String executable, |
| 236 List<String> arguments, |
| 237 Map<String, String> environmentOverrides) |
| 238 : super._(displayName, executable, arguments, environmentOverrides); |
| 239 |
| 240 Future<bool> get outputIsUpToDate { |
| 241 if (_neverSkipCompilation) return new Future.value(false); |
| 242 |
| 243 Future<List<Uri>> readDepsFile(String path) { |
| 244 var file = new io.File(new Path(path).toNativePath()); |
| 245 if (!file.existsSync()) { |
| 246 return new Future.value(null); |
| 247 } |
| 248 return file.readAsLines().then((List<String> lines) { |
| 249 var dependencies = new List<Uri>(); |
| 250 for (var line in lines) { |
| 251 line = line.trim(); |
| 252 if (line.length > 0) { |
| 253 dependencies.add(Uri.parse(line)); |
| 254 } |
| 255 } |
| 256 return dependencies; |
| 257 }); |
| 258 } |
| 259 |
| 260 return readDepsFile("$_outputFile.deps").then((dependencies) { |
| 261 if (dependencies != null) { |
| 262 dependencies.addAll(_bootstrapDependencies); |
| 263 var jsOutputLastModified = TestUtils.lastModifiedCache |
| 264 .getLastModified(new Uri(scheme: 'file', path: _outputFile)); |
| 265 if (jsOutputLastModified != null) { |
| 266 for (var dependency in dependencies) { |
| 267 var dependencyLastModified = |
| 268 TestUtils.lastModifiedCache.getLastModified(dependency); |
| 269 if (dependencyLastModified == null || |
| 270 dependencyLastModified.isAfter(jsOutputLastModified)) { |
| 271 return false; |
| 272 } |
| 273 } |
| 274 return true; |
| 275 } |
| 276 } |
| 277 return false; |
| 278 }); |
| 279 } |
| 280 |
| 281 void _buildHashCode(HashCodeBuilder builder) { |
| 282 super._buildHashCode(builder); |
| 283 builder.addJson(_outputFile); |
| 284 builder.addJson(_neverSkipCompilation); |
| 285 builder.addJson(_bootstrapDependencies); |
| 286 } |
| 287 |
| 288 bool _equal(CompilationCommand other) => |
| 289 super._equal(other) && |
| 290 _outputFile == other._outputFile && |
| 291 _neverSkipCompilation == other._neverSkipCompilation && |
| 292 deepJsonCompare(_bootstrapDependencies, other._bootstrapDependencies); |
| 293 } |
| 294 |
| 295 class KernelCompilationCommand extends CompilationCommand { |
| 296 KernelCompilationCommand._( |
| 297 String outputFile, |
| 298 bool neverSkipCompilation, |
| 299 List<Uri> bootstrapDependencies, |
| 300 String executable, |
| 301 List<String> arguments, |
| 302 Map<String, String> environmentOverrides) |
| 303 : super._('dartk', outputFile, neverSkipCompilation, |
| 304 bootstrapDependencies, executable, arguments, environmentOverrides); |
| 305 |
| 306 int get maxNumRetries => 1; |
| 307 } |
| 308 |
| 309 /// This is just a Pair(String, Map) class with hashCode and operator == |
| 310 class AddFlagsKey { |
| 311 final String flags; |
| 312 final Map env; |
| 313 AddFlagsKey(this.flags, this.env); |
| 314 // Just use object identity for environment map |
| 315 bool operator ==(Object other) => |
| 316 other is AddFlagsKey && flags == other.flags && env == other.env; |
| 317 int get hashCode => flags.hashCode ^ env.hashCode; |
| 318 } |
| 319 |
| 320 class ContentShellCommand extends ProcessCommand { |
| 321 ContentShellCommand._( |
| 322 String executable, |
| 323 String htmlFile, |
| 324 List<String> options, |
| 325 List<String> dartFlags, |
| 326 Map<String, String> environmentOverrides) |
| 327 : super._("content_shell", executable, _getArguments(options, htmlFile), |
| 328 _getEnvironment(environmentOverrides, dartFlags)); |
| 329 |
| 330 // Cache the modified environments in a map from the old environment and |
| 331 // the string of Dart flags to the new environment. Avoid creating new |
| 332 // environment object for each command object. |
| 333 static Map<AddFlagsKey, Map<String, String>> environments = {}; |
| 334 |
| 335 static Map<String, String> _getEnvironment( |
| 336 Map<String, String> env, List<String> dartFlags) { |
| 337 var needDartFlags = dartFlags != null && dartFlags.isNotEmpty; |
| 338 if (needDartFlags) { |
| 339 if (env == null) { |
| 340 env = const <String, String>{}; |
| 341 } |
| 342 var flags = dartFlags.join(' '); |
| 343 return environments.putIfAbsent( |
| 344 new AddFlagsKey(flags, env), |
| 345 () => new Map<String, String>.from(env) |
| 346 ..addAll({'DART_FLAGS': flags, 'DART_FORWARDING_PRINT': '1'})); |
| 347 } |
| 348 return env; |
| 349 } |
| 350 |
| 351 static List<String> _getArguments(List<String> options, String htmlFile) { |
| 352 var arguments = options.toList(); |
| 353 arguments.add(htmlFile); |
| 354 return arguments; |
| 355 } |
| 356 |
| 357 int get maxNumRetries => 3; |
| 358 } |
| 359 |
| 360 class BrowserTestCommand extends Command { |
| 361 Runtime get browser => configuration.runtime; |
| 362 final String url; |
| 363 final Configuration configuration; |
| 364 final bool retry; |
| 365 |
| 366 BrowserTestCommand._(this.url, this.configuration, this.retry) |
| 367 : super._(configuration.runtime.name); |
| 368 |
| 369 void _buildHashCode(HashCodeBuilder builder) { |
| 370 super._buildHashCode(builder); |
| 371 builder.addJson(browser.name); |
| 372 builder.addJson(url); |
| 373 builder.add(configuration); |
| 374 builder.add(retry); |
| 375 } |
| 376 |
| 377 bool _equal(BrowserTestCommand other) => |
| 378 super._equal(other) && |
| 379 browser == other.browser && |
| 380 url == other.url && |
| 381 identical(configuration, other.configuration) && |
| 382 retry == other.retry; |
| 383 |
| 384 String get reproductionCommand { |
| 385 var parts = [ |
| 386 io.Platform.resolvedExecutable, |
| 387 'tools/testing/dart/launch_browser.dart', |
| 388 browser.name, |
| 389 url |
| 390 ]; |
| 391 return parts.map(escapeCommandLineArgument).join(' '); |
| 392 } |
| 393 |
| 394 int get maxNumRetries => 4; |
| 395 } |
| 396 |
| 397 class BrowserHtmlTestCommand extends BrowserTestCommand { |
| 398 List<String> expectedMessages; |
| 399 BrowserHtmlTestCommand._(String url, Configuration configuration, |
| 400 this.expectedMessages, bool retry) |
| 401 : super._(url, configuration, retry); |
| 402 |
| 403 void _buildHashCode(HashCodeBuilder builder) { |
| 404 super._buildHashCode(builder); |
| 405 builder.addJson(expectedMessages); |
| 406 } |
| 407 |
| 408 bool _equal(BrowserHtmlTestCommand other) => |
| 409 super._equal(other) && |
| 410 identical(expectedMessages, other.expectedMessages); |
| 411 } |
| 412 |
| 413 class AnalysisCommand extends ProcessCommand { |
| 414 AnalysisCommand._(String executable, List<String> arguments, |
| 415 Map<String, String> environmentOverrides) |
| 416 : super._('dart2analyzer', executable, arguments, environmentOverrides); |
| 417 } |
| 418 |
| 419 class VmCommand extends ProcessCommand { |
| 420 VmCommand._(String executable, List<String> arguments, |
| 421 Map<String, String> environmentOverrides) |
| 422 : super._('vm', executable, arguments, environmentOverrides); |
| 423 } |
| 424 |
| 425 class VmBatchCommand extends ProcessCommand implements VmCommand { |
| 426 final String dartFile; |
| 427 final bool checked; |
| 428 |
| 429 VmBatchCommand._(String executable, String dartFile, List<String> arguments, |
| 430 Map<String, String> environmentOverrides, |
| 431 {this.checked: true}) |
| 432 : this.dartFile = dartFile, |
| 433 super._('vm-batch', executable, arguments, environmentOverrides); |
| 434 |
| 435 @override |
| 436 List<String> get batchArguments => |
| 437 checked ? ['--checked', dartFile] : [dartFile]; |
| 438 |
| 439 @override |
| 440 bool _equal(VmBatchCommand other) { |
| 441 return super._equal(other) && |
| 442 dartFile == other.dartFile && |
| 443 checked == other.checked; |
| 444 } |
| 445 |
| 446 @override |
| 447 void _buildHashCode(HashCodeBuilder builder) { |
| 448 super._buildHashCode(builder); |
| 449 builder.addJson(dartFile); |
| 450 builder.addJson(checked); |
| 451 } |
| 452 } |
| 453 |
| 454 class AdbPrecompilationCommand extends Command { |
| 455 final String precompiledRunnerFilename; |
| 456 final String processTestFilename; |
| 457 final String precompiledTestDirectory; |
| 458 final List<String> arguments; |
| 459 final bool useBlobs; |
| 460 |
| 461 AdbPrecompilationCommand._( |
| 462 this.precompiledRunnerFilename, |
| 463 this.processTestFilename, |
| 464 this.precompiledTestDirectory, |
| 465 this.arguments, |
| 466 this.useBlobs) |
| 467 : super._("adb_precompilation"); |
| 468 |
| 469 void _buildHashCode(HashCodeBuilder builder) { |
| 470 super._buildHashCode(builder); |
| 471 builder.add(precompiledRunnerFilename); |
| 472 builder.add(precompiledTestDirectory); |
| 473 builder.add(arguments); |
| 474 builder.add(useBlobs); |
| 475 } |
| 476 |
| 477 bool _equal(AdbPrecompilationCommand other) => |
| 478 super._equal(other) && |
| 479 precompiledRunnerFilename == other.precompiledRunnerFilename && |
| 480 useBlobs == other.useBlobs && |
| 481 arguments == other.arguments && |
| 482 precompiledTestDirectory == other.precompiledTestDirectory; |
| 483 |
| 484 String toString() => 'Steps to push precompiled runner and precompiled code ' |
| 485 'to an attached device. Uses (and requires) adb.'; |
| 486 } |
| 487 |
| 488 class JSCommandlineCommand extends ProcessCommand { |
| 489 JSCommandlineCommand._( |
| 490 String displayName, String executable, List<String> arguments, |
| 491 [Map<String, String> environmentOverrides = null]) |
| 492 : super._(displayName, executable, arguments, environmentOverrides); |
| 493 } |
| 494 |
| 495 class PubCommand extends ProcessCommand { |
| 496 final String command; |
| 497 |
| 498 PubCommand._(String pubCommand, String pubExecutable, |
| 499 String pubspecYamlDirectory, String pubCacheDirectory, List<String> args) |
| 500 : command = pubCommand, |
| 501 super._( |
| 502 'pub_$pubCommand', |
| 503 new io.File(pubExecutable).absolute.path, |
| 504 [pubCommand]..addAll(args), |
| 505 {'PUB_CACHE': pubCacheDirectory}, |
| 506 pubspecYamlDirectory); |
| 507 |
| 508 void _buildHashCode(HashCodeBuilder builder) { |
| 509 super._buildHashCode(builder); |
| 510 builder.addJson(command); |
| 511 } |
| 512 |
| 513 bool _equal(PubCommand other) => |
| 514 super._equal(other) && command == other.command; |
| 515 } |
| 516 |
| 517 /// [ScriptCommand]s are executed by dart code. |
| 518 abstract class ScriptCommand extends Command { |
| 519 ScriptCommand._(String displayName) : super._(displayName); |
| 520 |
| 521 Future<ScriptCommandOutputImpl> run(); |
| 522 } |
| 523 |
| 524 class CleanDirectoryCopyCommand extends ScriptCommand { |
| 525 final String _sourceDirectory; |
| 526 final String _destinationDirectory; |
| 527 |
| 528 CleanDirectoryCopyCommand._(this._sourceDirectory, this._destinationDirectory) |
| 529 : super._('dir_copy'); |
| 530 |
| 531 String get reproductionCommand => |
| 532 "Copying '$_sourceDirectory' to '$_destinationDirectory'."; |
| 533 |
| 534 Future<ScriptCommandOutputImpl> run() { |
| 535 var watch = new Stopwatch()..start(); |
| 536 |
| 537 var destination = new io.Directory(_destinationDirectory); |
| 538 |
| 539 return destination.exists().then((bool exists) { |
| 540 Future cleanDirectoryFuture; |
| 541 if (exists) { |
| 542 cleanDirectoryFuture = TestUtils.deleteDirectory(_destinationDirectory); |
| 543 } else { |
| 544 cleanDirectoryFuture = new Future.value(null); |
| 545 } |
| 546 return cleanDirectoryFuture.then((_) { |
| 547 return TestUtils.copyDirectory(_sourceDirectory, _destinationDirectory); |
| 548 }); |
| 549 }).then((_) { |
| 550 return new ScriptCommandOutputImpl( |
| 551 this, Expectation.pass, "", watch.elapsed); |
| 552 }).catchError((error) { |
| 553 return new ScriptCommandOutputImpl( |
| 554 this, Expectation.fail, "An error occured: $error.", watch.elapsed); |
| 555 }); |
| 556 } |
| 557 |
| 558 void _buildHashCode(HashCodeBuilder builder) { |
| 559 super._buildHashCode(builder); |
| 560 builder.addJson(_sourceDirectory); |
| 561 builder.addJson(_destinationDirectory); |
| 562 } |
| 563 |
| 564 bool _equal(CleanDirectoryCopyCommand other) => |
| 565 super._equal(other) && |
| 566 _sourceDirectory == other._sourceDirectory && |
| 567 _destinationDirectory == other._destinationDirectory; |
| 568 } |
| 569 |
| 570 /// Makes a symbolic link to another directory. |
| 571 class MakeSymlinkCommand extends ScriptCommand { |
| 572 String _link; |
| 573 String _target; |
| 574 |
| 575 MakeSymlinkCommand._(this._link, this._target) : super._('make_symlink'); |
| 576 |
| 577 String get reproductionCommand => |
| 578 "Make symbolic link '$_link' (target: $_target)'."; |
| 579 |
| 580 Future<ScriptCommandOutputImpl> run() { |
| 581 var watch = new Stopwatch()..start(); |
| 582 var targetFile = new io.Directory(_target); |
| 583 return targetFile.exists().then((bool targetExists) { |
| 584 if (!targetExists) { |
| 585 throw new Exception("Target '$_target' does not exist"); |
| 586 } |
| 587 var link = new io.Link(_link); |
| 588 |
| 589 return link.exists().then((bool exists) { |
| 590 if (exists) return link.delete(); |
| 591 }).then((_) => link.create(_target)); |
| 592 }).then((_) { |
| 593 return new ScriptCommandOutputImpl( |
| 594 this, Expectation.pass, "", watch.elapsed); |
| 595 }).catchError((error) { |
| 596 return new ScriptCommandOutputImpl( |
| 597 this, Expectation.fail, "An error occured: $error.", watch.elapsed); |
| 598 }); |
| 599 } |
| 600 |
| 601 void _buildHashCode(HashCodeBuilder builder) { |
| 602 super._buildHashCode(builder); |
| 603 builder.addJson(_link); |
| 604 builder.addJson(_target); |
| 605 } |
| 606 |
| 607 bool _equal(MakeSymlinkCommand other) => |
| 608 super._equal(other) && _link == other._link && _target == other._target; |
| 609 } |
OLD | NEW |