blob: 8db7dbc5aa39059fe0061cc35c229572851f2b12 [file] [log] [blame]
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
import 'package:yaml_edit/src/utils.dart';
import 'package:yaml_edit/yaml_edit.dart';
import 'test_utils.dart';
/// Interface for creating golden Test cases
class TestCases {
final List<_TestCase> testCases;
/// Creates a [TestCases] object based on test directory and golden directory
/// path.
static Future<TestCases> getTestCases(Uri testDirUri, Uri goldDirUri) async {
final testDir = Directory.fromUri(testDirUri);
if (!testDir.existsSync()) return TestCases([]);
/// Recursively grab all the files in the testing directory.
return TestCases(await testDir
.list(recursive: true, followLinks: false)
.where((entity) => entity.path.endsWith('.test'))
.map((entity) => entity.uri)
.map((inputUri) {
final inputWithoutExtension =
p.basenameWithoutExtension(inputUri.toFilePath());
final goldenUri = goldDirUri.resolve('./$inputWithoutExtension.golden');
return _TestCase(inputUri, goldenUri);
}).toList());
}
/// Tests all the [_TestCase]s if the golden files exist, create the golden
/// files otherwise.
void test() {
var tested = 0;
var created = 0;
for (final testCase in testCases) {
testCase.testOrCreate();
if (testCase.state == _TestCaseStates.testedGoldenFile) {
tested++;
} else if (testCase.state == _TestCaseStates.createdGoldenFile) {
created++;
}
}
print(
'Successfully tested $tested inputs against golden files, created $created golden files');
}
TestCases(this.testCases);
int get length => testCases.length;
}
/// Enum representing the different states of [_TestCase]s.
enum _TestCaseStates { initialized, createdGoldenFile, testedGoldenFile }
/// Interface for a golden test case. Handles the logic for test conduct/golden
/// test update accordingly.
class _TestCase {
final Uri inputUri;
final Uri goldenUri;
final List<String> states = [];
late String info;
late YamlEditor yamlBuilder;
late List<_YamlModification> modifications;
String inputLineEndings = '\n';
_TestCaseStates state = _TestCaseStates.initialized;
_TestCase(this.inputUri, this.goldenUri) {
final inputFile = File.fromUri(inputUri);
if (!inputFile.existsSync()) {
throw Exception('Input File does not exist!');
}
_initialize(inputFile);
}
/// Initializes the [_TestCase] by reading the corresponding [inputFile] and
/// parsing the different portions, and then running the input yaml against
/// the specified modifications.
///
/// Precondition: [inputFile] must exist, and inputs must be well-formatted.
void _initialize(File inputFile) {
final input = inputFile.readAsStringSync();
final inputLineEndings = getLineEnding(input);
final inputElements = input.split('---$inputLineEndings');
if (inputElements.length != 3) {
throw AssertionError('File ${inputFile.path} is not properly formatted.');
}
info = inputElements[0];
yamlBuilder = YamlEditor(inputElements[1]);
final rawModifications = _getValueFromYamlNode(loadYaml(inputElements[2]));
modifications = _parseModifications(rawModifications);
/// Adds the initial state as well, so we can check that the simplest
/// parse -> immediately dump does not affect the string.
states.add(yamlBuilder.toString());
_performModifications();
}
void _performModifications() {
for (final mod in modifications) {
_performModification(mod);
states.add(yamlBuilder.toString());
}
}
void _performModification(_YamlModification mod) {
switch (mod.method) {
case YamlModificationMethod.update:
yamlBuilder.update(mod.path, mod.value);
return;
case YamlModificationMethod.remove:
yamlBuilder.remove(mod.path);
return;
case YamlModificationMethod.appendTo:
yamlBuilder.appendToList(mod.path, mod.value);
return;
case YamlModificationMethod.prependTo:
yamlBuilder.prependToList(mod.path, mod.value);
return;
case YamlModificationMethod.insert:
yamlBuilder.insertIntoList(mod.path, mod.index, mod.value);
return;
case YamlModificationMethod.splice:
yamlBuilder.spliceList(mod.path, mod.index, mod.deleteCount, mod.value);
return;
}
}
void testOrCreate() {
final goldenFile = File.fromUri(goldenUri);
if (!goldenFile.existsSync()) {
createGoldenFile(goldenFile);
} else {
testGoldenFile(goldenFile);
}
}
void createGoldenFile(File goldenFile) {
/// Assumes user wants the golden file to have the same line endings as
/// the input file.
final goldenOutput = states.join('---$inputLineEndings');
goldenFile.writeAsStringSync(goldenOutput);
state = _TestCaseStates.createdGoldenFile;
}
/// Tests the golden file. Ensures that the number of states are the same, and
/// that the individual states are the same.
void testGoldenFile(File goldenFile) {
final inputFileName = p.basename(inputUri.toFilePath());
final golden = goldenFile.readAsStringSync();
final goldenStates = golden.split('---${getLineEnding(golden)}');
group('testing $inputFileName - input and golden files have', () {
test('same number of states', () {
expect(states.length, equals(goldenStates.length));
});
for (var i = 0; i < states.length; i++) {
test('same state $i', () {
expect(states[i], equals(goldenStates[i]));
});
}
});
state = _TestCaseStates.testedGoldenFile;
}
}
/// Converts [yamlList] into a Dart list.
List _getValueFromYamlList(YamlList yamlList) {
return yamlList.value.map((n) {
if (n is YamlNode) return _getValueFromYamlNode(n);
return n;
}).toList();
}
/// Converts [yamlMap] into a Dart Map.
Map _getValueFromYamlMap(YamlMap yamlMap) {
final keys = yamlMap.keys;
final result = {};
for (final key in keys) {
final value = yamlMap[key];
if (value is YamlNode) {
result[key] = _getValueFromYamlNode(value);
} else {
result[key] = value;
}
}
return result;
}
/// Converts a [YamlNode] into a Dart object.
dynamic _getValueFromYamlNode(YamlNode node) {
if (node is YamlList) {
return _getValueFromYamlList(node);
}
if (node is YamlMap) {
return _getValueFromYamlMap(node);
}
return node.value;
}
/// Converts the list of modifications from the raw input to [_YamlModification]
/// objects.
List<_YamlModification> _parseModifications(List<dynamic> modifications) {
return modifications.map((mod) {
Object? value;
var index = 0;
var deleteCount = 0;
final method = _getModificationMethod(mod[0] as String);
final path = mod[1];
if (method == YamlModificationMethod.appendTo ||
method == YamlModificationMethod.update ||
method == YamlModificationMethod.prependTo) {
value = mod[2];
} else if (method == YamlModificationMethod.insert) {
index = mod[2] as int;
value = mod[3];
} else if (method == YamlModificationMethod.splice) {
index = mod[2] as int;
deleteCount = mod[3] as int;
if (mod[4] is! List) {
throw ArgumentError('Invalid array ${mod[4]} used in splice');
}
value = mod[4];
}
return _YamlModification(method, path, index, value, deleteCount);
}).toList();
}
/// Gets the YAML modification method corresponding to [method]
YamlModificationMethod _getModificationMethod(String method) {
switch (method) {
case 'update':
return YamlModificationMethod.update;
case 'remove':
return YamlModificationMethod.remove;
case 'append':
case 'appendTo':
return YamlModificationMethod.appendTo;
case 'prepend':
case 'prependTo':
return YamlModificationMethod.prependTo;
case 'insert':
case 'insertIn':
return YamlModificationMethod.insert;
case 'splice':
return YamlModificationMethod.splice;
default:
throw Exception('$method not recognized!');
}
}
/// Class representing an abstract YAML modification to be performed
class _YamlModification {
final YamlModificationMethod method;
final List<Object?> path;
final int index;
final dynamic value;
final int deleteCount;
_YamlModification(
this.method, this.path, this.index, this.value, this.deleteCount);
@override
String toString() =>
'method: $method, path: $path, index: $index, value: $value, deleteCount: $deleteCount';
}