diff --git a/build/lib/src/builder/logging.dart b/build/lib/src/builder/logging.dart index b3c9aff68..8f21d6d01 100644 --- a/build/lib/src/builder/logging.dart +++ b/build/lib/src/builder/logging.dart @@ -9,7 +9,27 @@ const Symbol logKey = #buildLog; /// Will be `null` when not running within a build. Logger get log => Zone.current[logKey] as Logger; -T scopeLog(T fn(), Logger log) => runZoned(fn, zoneSpecification: - new ZoneSpecification(print: (self, parent, zone, message) { - log.info(message); - }), zoneValues: {logKey: log}); +/// Runs [fn] in an error handling [Zone]. +/// +/// Any calls to [print] will be logged with `log.info`, and any errors will be +/// logged with `log.severe`. +/// +/// Completes with the first error or result of `fn`, whichever comes first. +Future scopeLogAsync(Future fn(), Logger log) { + var done = new Completer(); + runZoned(fn, + zoneSpecification: + new ZoneSpecification(print: (self, parent, zone, message) { + log.info(message); + }), + zoneValues: {logKey: log}, + onError: (Object e, StackTrace s) { + log.severe('', e); + if (done.isCompleted) return; + done.completeError(e, s); + }).then((result) { + if (done.isCompleted) return; + done.complete(result); + }); + return done.future; +} diff --git a/build/lib/src/generate/run_builder.dart b/build/lib/src/generate/run_builder.dart index 10a396909..ea3f045c7 100644 --- a/build/lib/src/generate/run_builder.dart +++ b/build/lib/src/generate/run_builder.dart @@ -46,7 +46,7 @@ Future runBuilder(Builder builder, Iterable inputs, } } - await scopeLog(() => Future.wait(inputs.map(buildForInput)), logger); + await scopeLogAsync(() => Future.wait(inputs.map(buildForInput)), logger); if (shouldDisposeResourceManager) { await resourceManager.disposeAll(); diff --git a/build_compilers/lib/src/dev_compiler_builder.dart b/build_compilers/lib/src/dev_compiler_builder.dart index 7a887e04d..d89dfd2da 100644 --- a/build_compilers/lib/src/dev_compiler_builder.dart +++ b/build_compilers/lib/src/dev_compiler_builder.dart @@ -40,9 +40,9 @@ class DevCompilerBuilder implements Builder { try { await createDevCompilerModule(module, buildStep); } on DartDevcCompilationException catch (e) { - log.warning('Error compiling ${module.jsId}:\n$e'); await buildStep.writeAsString( buildStep.inputId.changeExtension(jsModuleErrorsExtension), '$e'); + log.severe('', e); } } } diff --git a/build_compilers/lib/src/modules.dart b/build_compilers/lib/src/modules.dart index 98a9fc083..0aa4c275e 100644 --- a/build_compilers/lib/src/modules.dart +++ b/build_compilers/lib/src/modules.dart @@ -116,9 +116,14 @@ class Module extends Object with _$ModuleSerializerMixin { var next = modulesToCrawl.last; modulesToCrawl.remove(next); if (transitiveDeps.containsKey(next)) continue; - var module = new Module.fromJson(JSON.decode( - await reader.readAsString(next.changeExtension(moduleExtension))) - as Map); + var nextModuleId = next.changeExtension(moduleExtension); + if (!await reader.canRead(nextModuleId)) { + log.warning('Missing module $nextModuleId'); + continue; + } + var module = new Module.fromJson( + JSON.decode(await reader.readAsString(nextModuleId)) + as Map); transitiveDeps[next] = module; modulesToCrawl.addAll(module.directDependencies); } diff --git a/build_runner/lib/src/asset/reader.dart b/build_runner/lib/src/asset/reader.dart index 6ad67fcb6..5e39c339d 100644 --- a/build_runner/lib/src/asset/reader.dart +++ b/build_runner/lib/src/asset/reader.dart @@ -35,8 +35,13 @@ class SinglePhaseReader implements AssetReader { Iterable get globsRan => _globsRan; bool _isReadable(AssetId id) { + _assetsRead.add(id); var node = _assetGraph.get(id); - if (node == null) return false; + if (node == null) { + _assetGraph.add(new SyntheticAssetNode(id)); + return false; + } + if (node is SyntheticAssetNode) return false; if (node is! GeneratedAssetNode) return true; return (node as GeneratedAssetNode).phaseNumber < _phaseNumber; } @@ -44,7 +49,6 @@ class SinglePhaseReader implements AssetReader { @override Future canRead(AssetId id) async { if (!_isReadable(id)) return new Future.value(false); - _assetsRead.add(id); await _ensureAssetIsBuilt(id); var node = _assetGraph.get(id); if (node is GeneratedAssetNode) { @@ -59,7 +63,6 @@ class SinglePhaseReader implements AssetReader { @override Future> readAsBytes(AssetId id) async { if (!_isReadable(id)) throw new AssetNotFoundException(id); - _assetsRead.add(id); await _ensureAssetIsBuilt(id); return _delegate.readAsBytes(id); } @@ -67,7 +70,6 @@ class SinglePhaseReader implements AssetReader { @override Future readAsString(AssetId id, {Encoding encoding: UTF8}) async { if (!_isReadable(id)) throw new AssetNotFoundException(id); - _assetsRead.add(id); await _ensureAssetIsBuilt(id); return _delegate.readAsString(id, encoding: encoding); } diff --git a/build_runner/lib/src/asset_graph/graph.dart b/build_runner/lib/src/asset_graph/graph.dart index 0e9ba60d8..08ba7ae27 100644 --- a/build_runner/lib/src/asset_graph/graph.dart +++ b/build_runner/lib/src/asset_graph/graph.dart @@ -60,10 +60,17 @@ class AssetGraph { /// /// Throws a [StateError] if it already exists in the graph. void _add(AssetNode node) { - if (contains(node.id)) { - throw new StateError( - 'Tried to add node ${node.id} to the asset graph but it already ' - 'exists.'); + var existing = get(node.id); + if (existing != null) { + if (existing is SyntheticAssetNode) { + _remove(existing.id); + node.outputs.addAll(existing.outputs); + node.primaryOutputs.addAll(existing.primaryOutputs); + } else { + throw new StateError( + 'Tried to add node ${node.id} to the asset graph but it already ' + 'exists.'); + } } _nodesByPackage.putIfAbsent(node.id.package, () => {})[node.id.path] = node; } @@ -84,8 +91,9 @@ class AssetGraph { allNodes.where((n) => n is GeneratedAssetNode).map((n) => n.id); /// All the source files in the graph. - Iterable get sources => - allNodes.where((n) => n is! GeneratedAssetNode).map((n) => n.id); + Iterable get sources => allNodes + .where((n) => n is! GeneratedAssetNode && n is! SyntheticAssetNode) + .map((n) => n.id); /// Updates graph structure, invalidating and deleting any outputs that were /// affected. @@ -105,31 +113,32 @@ class AssetGraph { // Builds up `idsToDelete` and `idsToRemove` by recursively invalidating // the outputs of `id`. void clearNodeAndDeps(AssetId id, ChangeType rootChangeType, - {AssetId parent, bool rootIsSource}) { + {bool rootIsSource}) { var node = this.get(id); if (node == null) return; if (!invalidatedIds.add(id)) return; - if (parent == null) rootIsSource = node is! GeneratedAssetNode; - - // Update all outputs of this asset as well. - for (var output in node.outputs) { - clearNodeAndDeps(output, rootChangeType, - parent: node.id, rootIsSource: rootIsSource); - } + rootIsSource ??= node is! GeneratedAssetNode; if (node is GeneratedAssetNode) { idsToDelete.add(id); if (rootIsSource && rootChangeType == ChangeType.REMOVE && - node.primaryInput == parent) { + idsToRemove.contains(node.primaryInput)) { idsToRemove.add(id); } else { node.needsUpdate = true; + node.wasOutput = false; + node.globs = new Set(); } } else { // This is a source if (rootChangeType == ChangeType.REMOVE) idsToRemove.add(id); } + + // Update all outputs of this asset as well. + for (var output in node.outputs) { + clearNodeAndDeps(output, rootChangeType, rootIsSource: rootIsSource); + } } updates.forEach(clearNodeAndDeps); diff --git a/build_runner/lib/src/asset_graph/node.dart b/build_runner/lib/src/asset_graph/node.dart index 9c64576b3..2178d0980 100644 --- a/build_runner/lib/src/asset_graph/node.dart +++ b/build_runner/lib/src/asset_graph/node.dart @@ -23,6 +23,18 @@ class AssetNode { String toString() => 'AssetNode: $id'; } +/// A node which is not a generated or source asset. +/// +/// Typically these are created as a result of `canRead` calls for assets that +/// don't exist in the graph. We still need to set up proper dependencies so +/// that if that asset gets added later the outputs are properly invalidated. +class SyntheticAssetNode extends AssetNode { + SyntheticAssetNode(AssetId id) : super(id); + + @override + String toString() => 'SyntheticAssetNode: $id'; +} + /// A generated node in the asset graph. class GeneratedAssetNode extends AssetNode { /// The phase which generated this asset. diff --git a/build_runner/lib/src/asset_graph/serialization.dart b/build_runner/lib/src/asset_graph/serialization.dart index 34f4865d9..be1d08b0c 100644 --- a/build_runner/lib/src/asset_graph/serialization.dart +++ b/build_runner/lib/src/asset_graph/serialization.dart @@ -8,7 +8,7 @@ part of 'graph.dart'; /// /// This should be incremented any time the serialize/deserialize formats /// change. -const _version = 7; +const _version = 8; /// Deserializes an [AssetGraph] from a [Map]. class _AssetGraphDeserializer { @@ -43,16 +43,21 @@ class _AssetGraphDeserializer { AssetNode _deserializeAssetNode(List serializedNode) { AssetNode node; - if (serializedNode.length == 3) { - node = new AssetNode(_idToAssetId[serializedNode[0]]); - } else if (serializedNode.length == 8) { + var id = _idToAssetId[serializedNode[0]]; + if (serializedNode.length == 4) { + if (_deserializeBool(serializedNode[3] as int)) { + node = new SyntheticAssetNode(id); + } else { + node = new AssetNode(id); + } + } else if (serializedNode.length == 9) { node = new GeneratedAssetNode( - serializedNode[5] as int, - _idToAssetId[serializedNode[3]], - _deserializeBool(serializedNode[7] as int), - _deserializeBool(serializedNode[4] as int), - _idToAssetId[serializedNode[0]], - globs: (serializedNode[6] as Iterable) + serializedNode[6] as int, + _idToAssetId[serializedNode[4]], + _deserializeBool(serializedNode[8] as int), + _deserializeBool(serializedNode[5] as int), + id, + globs: (serializedNode[7] as Iterable) .map((pattern) => new Glob(pattern)) .toSet(), ); @@ -125,7 +130,7 @@ class _WrappedAssetNode extends Object with ListMixin implements List { _WrappedAssetNode(this.node, this.serializer); @override - int get length => 3; + int get length => 4; @override set length(_) => throw new UnsupportedError( 'length setter not unsupported for WrappedAssetNode'); @@ -141,6 +146,8 @@ class _WrappedAssetNode extends Object with ListMixin implements List { return node.primaryOutputs .map((id) => serializer._assetIdToId[id]) .toList(); + case 3: + return _serializeBool(node is SyntheticAssetNode); default: throw new RangeError.index(index, this); } @@ -167,22 +174,22 @@ class _WrappedGeneratedAssetNode extends _WrappedAssetNode { Object operator [](int index) { if (index < super.length) return super[index]; switch (index) { - case 3: + case 4: return generatedNode.primaryInput != null ? serializer._assetIdToId[generatedNode.primaryInput] : null; - case 4: - return _serializeBool(generatedNode.wasOutput); case 5: - return generatedNode.phaseNumber; + return _serializeBool(generatedNode.wasOutput); case 6: - return generatedNode.globs.map((glob) => glob.pattern).toList(); + return generatedNode.phaseNumber; case 7: + return generatedNode.globs.map((glob) => glob.pattern).toList(); + case 8: return _serializeBool(generatedNode.needsUpdate); default: throw new RangeError.index(index, this); } } - - static int _serializeBool(bool value) => value ? 1 : 0; } + +int _serializeBool(bool value) => value ? 1 : 0; diff --git a/build_runner/lib/src/generate/build_definition.dart b/build_runner/lib/src/generate/build_definition.dart index 6f3d0bce3..76ddc8080 100644 --- a/build_runner/lib/src/generate/build_definition.dart +++ b/build_runner/lib/src/generate/build_definition.dart @@ -153,12 +153,17 @@ class _Loader { } } - var newSources = inputSources - .difference(assetGraph.allNodes.map((node) => node.id).toSet()); + var newSources = inputSources.difference(assetGraph.allNodes + .where((node) => node is! SyntheticAssetNode) + .map((node) => node.id) + .toSet()); addUpdates(newSources, ChangeType.ADD); var removedAssets = assetGraph.allNodes - .where((n) => - n is! GeneratedAssetNode || (n as GeneratedAssetNode).wasOutput) + .where((n) { + if (n is SyntheticAssetNode) return false; + if (n is GeneratedAssetNode) return n.wasOutput; + return true; + }) .map((n) => n.id) .where((id) => !allSources.contains((id))); diff --git a/build_runner/lib/src/generate/build_impl.dart b/build_runner/lib/src/generate/build_impl.dart index 99b0911b3..5ffe78124 100644 --- a/build_runner/lib/src/generate/build_impl.dart +++ b/build_runner/lib/src/generate/build_impl.dart @@ -227,6 +227,7 @@ class BuildImpl { var ids = new Set(); await Future .wait(_assetGraph.packageNodes(inputSet.package).map((node) async { + if (node is SyntheticAssetNode) return; if (!inputSet.matches(node.id)) return; if (node is GeneratedAssetNode) { if (node.phaseNumber >= phaseNumber) return; @@ -310,8 +311,9 @@ class BuildImpl { input.package, (phase, input) => _runLazyPhaseForInput(phase, input, resourceManager)); var wrappedWriter = new AssetWriterSpy(_writer); + var logger = new Logger('$builder on $input'); await runBuilder(builder, [input], wrappedReader, wrappedWriter, _resolvers, - resourceManager: resourceManager); + logger: logger, resourceManager: resourceManager); // Reset the state for all the `builderOutputs` nodes based on what was // read and written. @@ -339,11 +341,15 @@ class BuildImpl { (phase, input) => _runLazyPhaseForInput(phase, input, resourceManager)); var wrappedWriter = new AssetWriterSpy(_writer); - var logger = new Logger('runBuilder'); + var logger = new Logger('$builder on $package'); var buildStep = new BuildStepImpl(null, builderOutputs, wrappedReader, wrappedWriter, _packageGraph.root.name, _resolvers, resourceManager); try { - await scopeLog(() => builder.build(buildStep), logger); + // Wrapping in `new Future.value` to work around + // https://github.com/dart-lang/sdk/issues/31237, users might return + // synchronously and not have any analysis errors today. + await scopeLogAsync( + () => new Future.value(builder.build(buildStep)), logger); } finally { await buildStep.complete(); } diff --git a/build_runner/test/asset_graph/graph_test.dart b/build_runner/test/asset_graph/graph_test.dart index e50d496ba..bdc221ecb 100644 --- a/build_runner/test/asset_graph/graph_test.dart +++ b/build_runner/test/asset_graph/graph_test.dart @@ -81,6 +81,11 @@ void main() { 0, node.id, g % 2 == 1, g % 2 == 0, makeAssetId()); node.outputs.add(generatedNode.id); node.primaryOutputs.add(generatedNode.id); + + var syntheticNode = new SyntheticAssetNode(makeAssetId()); + syntheticNode.outputs.add(generatedNode.id); + + graph.add(syntheticNode); graph.add(generatedNode); } } @@ -103,6 +108,8 @@ void main() { final buildActions = [new BuildAction(new CopyBuilder(), 'foo')]; final primaryInputId = makeAssetId('foo|file'); final primaryOutputId = makeAssetId('foo|file.copy'); + final syntheticId = makeAssetId('foo|synthetic'); + final syntheticOutputId = makeAssetId('foo|synthetic.copy'); setUp(() { graph = new AssetGraph.build( @@ -150,6 +157,35 @@ void main() { expect(graph.contains(primaryOutputId), isTrue); expect(deletes, equals([primaryOutputId])); }); + + test('add new primary input which replaces a synthetic node', () async { + var syntheticNode = new SyntheticAssetNode(syntheticId); + graph.add(syntheticNode); + expect(graph.get(syntheticId), syntheticNode); + + var changes = {syntheticId: ChangeType.ADD}; + await graph.updateAndInvalidate(buildActions, changes, 'foo', null); + + expect(graph.contains(syntheticId), isTrue); + expect(graph.get(syntheticId), + isNot(new isInstanceOf())); + expect(graph.contains(syntheticOutputId), isTrue); + }); + + test('add new generated asset which replaces a synthetic node', + () async { + var syntheticNode = new SyntheticAssetNode(syntheticOutputId); + graph.add(syntheticNode); + expect(graph.get(syntheticOutputId), syntheticNode); + + var changes = {syntheticId: ChangeType.ADD}; + await graph.updateAndInvalidate(buildActions, changes, 'foo', null); + + expect(graph.contains(syntheticOutputId), isTrue); + expect(graph.get(syntheticOutputId), + new isInstanceOf()); + expect(graph.contains(syntheticOutputId), isTrue); + }); }); }); }); diff --git a/build_runner/test/common/common.dart b/build_runner/test/common/common.dart index 2ac468f9c..56be29ec2 100644 --- a/build_runner/test/common/common.dart +++ b/build_runner/test/common/common.dart @@ -44,7 +44,7 @@ class TxtFilePackageBuilder extends PackageBuilder { TxtFilePackageBuilder(this.package, this.outputContents); @override - build(BuildStep buildStep) { + Future build(BuildStep buildStep) async { outputContents.forEach((path, content) => buildStep.writeAsString(new AssetId(package, path), content)); } diff --git a/build_runner/test/common/matchers.dart b/build_runner/test/common/matchers.dart index a124ed4af..abec201fe 100644 --- a/build_runner/test/common/matchers.dart +++ b/build_runner/test/common/matchers.dart @@ -31,6 +31,7 @@ class _AssetGraphMatcher extends Matcher { } for (var node in graph.allNodes) { var expectedNode = _expected.get(node.id); + if (node.runtimeType != expectedNode.runtimeType) return false; if (expectedNode == null || expectedNode.id != node.id) return false; if (!unorderedEquals(node.outputs).matches(expectedNode.outputs, null)) { return false; diff --git a/build_test/lib/build_test.dart b/build_test/lib/build_test.dart index 38cf6b752..c748eb0f7 100644 --- a/build_test/lib/build_test.dart +++ b/build_test/lib/build_test.dart @@ -2,7 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -export 'package:build/src/builder/logging.dart' show scopeLog; +export 'package:build/src/builder/logging.dart' show scopeLogAsync; export 'src/assets.dart'; export 'src/copy_builder.dart'; diff --git a/build_test/lib/src/record_logs.dart b/build_test/lib/src/record_logs.dart index dcaf13a49..f6449dbc8 100644 --- a/build_test/lib/src/record_logs.dart +++ b/build_test/lib/src/record_logs.dart @@ -25,7 +25,7 @@ import 'package:logging/logging.dart'; Stream recordLogs(dynamic run(), {String name: ''}) { final logger = new Logger(name); Timer.run(() async { - await scopeLog(run, logger); + await scopeLogAsync(() => new Future.value(run()), logger); logger.clearListeners(); }); return logger.onRecord; diff --git a/e2e_example/test/integration_test.dart b/e2e_example/test/integration_test.dart index 84315949d..ad96d8dab 100644 --- a/e2e_example/test/integration_test.dart +++ b/e2e_example/test/integration_test.dart @@ -27,13 +27,13 @@ void main() { .transform(UTF8.decoder) .transform(const LineSplitter()) .asBroadcastStream(); - stdOutLines.listen(print); + stdOutLines.listen((l) => print('stdout: $l')); stdErrLines = process.stderr .transform(UTF8.decoder) .transform(const LineSplitter()) .asBroadcastStream(); - stdErrLines.listen(print); + stdErrLines.listen((l) => print('stderr: $l')); await nextSuccessfulBuild; }); @@ -58,12 +58,15 @@ void main() { group('File changes', () { bool gitWasClean = false; - void ensureCleanGitClient() { + bool gitIsClean() { var gitStatus = Process.runSync('git', ['status', '.']).stdout as String; - gitWasClean = gitStatus.contains('nothing to commit, working tree clean'); + return gitStatus.contains('nothing to commit, working tree clean'); + } + + void ensureCleanGitClient() { + gitWasClean = gitIsClean(); expect(gitWasClean, isTrue, - reason: 'Not running on a clean git client, aborting test.\n' - '`git status .` gave:\n$gitStatus'); + reason: 'Not running on a clean git client, aborting test.\n'); } setUp(() async { @@ -72,6 +75,8 @@ void main() { tearDown(() async { if (gitWasClean) { + if (gitIsClean()) return; + // Reset our state after each test, assuming we didn't abandon tests due // to a non-pristine git environment. Process.runSync('git', ['checkout', 'HEAD', '--', '.']); @@ -79,6 +84,7 @@ void main() { var gitStatus = Process .runSync('git', ['status', '--porcelain', '.']).stdout as String; + var nextBuild = nextSuccessfulBuild; var untracked = gitStatus .split('\n') .where((line) => line.startsWith('??')) @@ -91,43 +97,65 @@ void main() { for (var path in untracked) { Process.runSync('rm', [path]); } - await nextSuccessfulBuild; + await nextBuild; } }); test('edit test to fail and rerun', () async { + var nextBuild = nextSuccessfulBuild; await replaceAllInFile( 'test/common/message.dart', 'Hello World!', 'Goodbye World!'); - await nextSuccessfulBuild; + await nextBuild; await expectTestsFail(); }); test('edit dependency lib causing test to fail and rerun', () async { + var nextBuild = nextSuccessfulBuild; await replaceAllInFile('lib/app.dart', 'Hello World!', 'Goodbye World!'); - await nextSuccessfulBuild; + await nextBuild; await expectTestsFail(); }); test('create new test', () async { + var nextBuild = nextSuccessfulBuild; await createFile(p.join('test', 'other_test.dart'), basicTestContents); - await nextSuccessfulBuild; + await nextBuild; await expectTestsPass(3); }); test('delete test', () async { + var nextBuild = nextSuccessfulBuild; await deleteFile(p.join('test', 'subdir', 'subdir_test.dart')); - await nextSuccessfulBuild; + await nextBuild; await expectTestsPass(1); }); - test('build failures can be fixed', () async { + test('ddc errors can be fixed', () async { var path = p.join('test', 'common', 'message.dart'); + var error = nextStdErrLine('Error compiling dartdevc module:' + 'e2e_example|test/hello_world_test.js'); + var nextBuild = nextSuccessfulBuild; await deleteFile(path); - await nextFailedBuild; + await error; + await nextBuild; + + nextBuild = nextSuccessfulBuild; await createFile(path, "String get message => 'Hello World!';"); - await nextSuccessfulBuild; + await nextBuild; await expectTestsPass(); - }, skip: 'https://github.com/dart-lang/build/issues/548'); + }); + + test('build errors can be fixed', () async { + var path = p.join('lib', 'expected.fail'); + + var nextBuild = nextFailedBuild; + await createFile(path, 'some error'); + await nextBuild; + + nextBuild = nextSuccessfulBuild; + await deleteFile(path); + await nextBuild; + }); }); } @@ -137,6 +165,9 @@ Future get nextSuccessfulBuild => Future get nextFailedBuild => stdErrLines.firstWhere((line) => line.contains('Build: Failed after')); +Future nextStdErrLine(String message) => + stdErrLines.firstWhere((line) => line.contains(message)); + Future runTests() => Process.run('pub', ['run', 'test', '--pub-serve', '8081', '-p', 'chrome']); diff --git a/e2e_example/tool/build.dart b/e2e_example/tool/build.dart index 363d495ae..7f35dcbbf 100644 --- a/e2e_example/tool/build.dart +++ b/e2e_example/tool/build.dart @@ -16,6 +16,7 @@ Future main() async { var buildActions = [ new BuildAction(new TestBootstrapBuilder(), graph.root.name, inputs: ['test/**_test.dart']), + new BuildAction(new ThrowingBuilder(), graph.root.name), ]; void addBuilderForAll(Builder builder, String inputExtension) { @@ -53,3 +54,15 @@ Future main() async { await server.close(); await testServer.close(); } + +class ThrowingBuilder extends Builder { + @override + final buildExtensions = { + '.fail': ['.fail.message'] + }; + + @override + Future build(BuildStep buildStep) async { + throw await buildStep.readAsString(buildStep.inputId); + } +}