Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions pkgs/yaml_edit/test/crash_test/crash_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// 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.

@TestOn('vm')
library;

import 'dart:io';
import 'dart:isolate';

import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
import 'package:yaml_edit/yaml_edit.dart';

final _scalarStyles = [
ScalarStyle.ANY,
ScalarStyle.PLAIN,
ScalarStyle.LITERAL,
ScalarStyle.FOLDED,
ScalarStyle.SINGLE_QUOTED,
ScalarStyle.DOUBLE_QUOTED,
];

/// Files with tests that are broken, so we have to skip them
final _skippedFiles = [
'block_strings.yaml',
'complex.yaml',
'explicit_key_value.yaml',
'mangled_json.yaml',
'simple_comments.yaml',
];

/// The crash tests will attempt to enumerate all JSON paths in each input
/// document and then proceed to make arbitrary mutations trying to see if
/// [YamlEditor] will crash. Arbitrary mutations include:
/// - Remove each JSON path
/// - Prepend and append to each list and map.
/// - Set each string to 'hello world'
/// - Set all numbers to 42
///
/// Input documents are loaded from: test/crash_test/testdata/*.yaml
Future<void> main() async {
final packageUri = await Isolate.resolvePackageUri(
Uri.parse('package:yaml_edit/yaml_edit.dart'));

final testdataUri = packageUri!.resolve('../test/crash_test/testdata/');
final testFiles = Directory.fromUri(testdataUri)
.listSync()
.whereType<File>()
.where((f) => f.path.endsWith('.yaml'))
.toList();

for (final f in testFiles) {
final fileName = f.uri.pathSegments.last;
final input = f.readAsStringSync();
final root = YamlEditor(input);

test('$fileName is valid YAML', () {
loadYamlNode(input);
});

if (_skippedFiles.contains(fileName)) {
test(
'crash_test.dart for $fileName',
() {},
skip: 'Known failures in "$fileName"',
);
continue;
}

for (final (path, node) in _allJsonPaths(root.parseAt([]))) {
_testJsonPath(fileName, input, path, node);
}
}
}

void _testJsonPath(
String fileName,
String input,
Iterable<Object?> path,
YamlNode node,
) {
final editorName = 'YamlEditor($fileName)';

// Try to remove the node
test('$editorName.remove($path)', () {
final editor = YamlEditor(input);
editor.remove(path);
});

// Try to update path to a string
test('$editorName.update($path, \'updated string\')', () {
final editor = YamlEditor(input);
editor.update(path, 'updated string');
});

// Try to update path to an integer
test('$editorName.update($path, 42)', () {
final editor = YamlEditor(input);
editor.update(path, 42);
});

// Try to set a multi-line string for each style
for (final style in _scalarStyles) {
test('$editorName.update($path, \'foo\\nbar\') as $style', () {
final editor = YamlEditor(input);
editor.update(path, YamlScalar.wrap('foo\nbar', style: style));
});
}

// If it's a list, we try to insert into the list for each index
if (node is YamlList) {
for (var i = 0; i < node.length + 1; i++) {
test('$editorName.insertIntoList($path, $i, 42)', () {
final editor = YamlEditor(input);
editor.insertIntoList(path, i, 42);
});

test('$editorName.insertIntoList($path, $i, \'new string\')', () {
final editor = YamlEditor(input);
editor.insertIntoList(path, i, 'new string');
});

for (final style in _scalarStyles) {
test('$editorName.insertIntoList($path, $i, \'foo\\nbar\') as $style',
() {
final editor = YamlEditor(input);
editor.insertIntoList(
path,
i,
YamlScalar.wrap(
'foo\nbar',
style: style,
));
});
}
}
}

// If it's a map, we try to insert a new key (if the new-key name isn't used)
if (node is YamlMap && !node.containsKey('new-key')) {
final newPath = [...path, 'new-key'];

test('$editorName.update($newPath, 42)', () {
final editor = YamlEditor(input);
editor.update(newPath, 42);
});

test('$editorName.update($newPath, \'new string\')', () {
final editor = YamlEditor(input);
editor.update(newPath, 'new string');
});

for (final style in _scalarStyles) {
test('$editorName.update($newPath, \'foo\\nbar\') as $style', () {
final editor = YamlEditor(input);
editor.update(
newPath,
YamlScalar.wrap(
'foo\nbar',
style: style,
));
});
}
}
}

Iterable<(Iterable<Object?>, YamlNode)> _allJsonPaths(
YamlNode node, [
Iterable<Object?> parents = const [],
]) sync* {
yield (parents, node);

if (node is YamlMap) {
for (final entry in node.nodes.entries) {
final key = entry.key as YamlNode;
final value = entry.value;
yield* _allJsonPaths(value, [...parents, key.value]);
}
} else if (node is YamlList) {
for (var i = 0; i < node.nodes.length; i++) {
final value = node.nodes[i];
yield* _allJsonPaths(value, [...parents, i]);
}
}
}
41 changes: 41 additions & 0 deletions pkgs/yaml_edit/test/crash_test/testdata/block_strings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
block-strings: # annoying comment (maybe)
- folded:
- clip: >
first line

skipped line


# This is a comment
# this too!
# And this one
- strip: >+ # We can have comments here
first line

skipped line


- keep: >-
first line

skipped line
# comment why not!
# and again
- literal:
- clip: |
first line

skipped line


- strip: |+
first line

skipped line


- keep: |-
first line

skipped line

93 changes: 93 additions & 0 deletions pkgs/yaml_edit/test/crash_test/testdata/complex.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Start comment
? string
# why not here too!
: hello world
# top-level comment
# comment again
? block-strings # annoying comment (maybe)
: - folded:
- clip: >
first line

skipped line


# This is a comment
# this too!
# And this one
- strip: >+ # We can have comments here
first line

skipped line


- keep: >-
first line

skipped line
# comment why not!
# and again
- literal:
- clip: |
first line

skipped line


- strip: |+
first line

skipped line


- keep: |-
first line

skipped line


? key
: value # Note: this works
# This too ?
? map
: k1: 1
k2: 2
k3: 3
? list
: - 1
- 2
- 3
? inlineMap
: {k1: 1, k2: 2, k3: 3}
? inlineList
: [1, 2, 3]
? complex-object
: foo: 42
bar:
- 'test test'
- |
hello world
- "test string"
- {
a: 1,
b:
'hello world'
}
? json-with-comments
: {
"key": "value",
'string with newline': 'hello
world'
, 32 : 42
, list :
[ # We can make multi-line strings inline!
'foo


bar' # comment before comma
, -32.4
# Comment again.
, # Comment on the comma!
# trailing comma, why not
]
}
12 changes: 12 additions & 0 deletions pkgs/yaml_edit/test/crash_test/testdata/explicit_key_value.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
? map
: k1: 1
k2: 2
k3: 3
? list
: - 1
- 2
- 3
? inlineMap
: {k1: 1, k2: 2, k3: 3}
? inlineList
: [1, 2, 3]
12 changes: 12 additions & 0 deletions pkgs/yaml_edit/test/crash_test/testdata/json.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"string": "hello world",
"map": {
"foo": 42,
"bar": "baz"
},
"list": [
1,
2,
3
]
}
15 changes: 15 additions & 0 deletions pkgs/yaml_edit/test/crash_test/testdata/json_comments.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Initial comment
{
"string": "hello world", # Great message
"map": {
"foo": 42, # Fantastic number
"bar": "baz"
},
"list": [
1, # This is a good list
2, # This comment is good
# But this?
# Or this?
3
]
}
15 changes: 15 additions & 0 deletions pkgs/yaml_edit/test/crash_test/testdata/mangled_json.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"string":
"hello world",
"map": {
"foo":
42
,"bar":
"baz"
}
,"list": [
1,2
,
3
,]
}
8 changes: 8 additions & 0 deletions pkgs/yaml_edit/test/crash_test/testdata/simple.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
string: 'hello world'
map:
foo: 42
bar: 'baz'
list:
- 1
- 2
- 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Initial comment
string: 'hello world' # Comment
map:
foo: 42 # Best number
bar: 'baz'
list: # Great starter list
- 1
- 2
- 3