// ignore_for_file: deprecated_member_use_from_same_package import 'dart:async'; import 'dart:math' show Random, min; import 'package:meta/meta.dart'; import 'package:test/test.dart'; import 'package:xml/xml.dart'; import 'package:xml/xml_events.dart'; import 'utils/assertions.dart'; import 'utils/examples.dart'; import 'utils/matchers.dart'; @isTestGroup void chunkedTests( String title, T Function() factory, Stream Function(T input, int Function() splitter) chunker, FutureOr Function(Stream stream) callback, ) => group(title, () { for (var i = 1; i <= 512; i *= 2) { test( 'chunks equally sized $i', () => callback(chunker(factory(), () => i)), ); } final random = Random(title.hashCode); for (var i = 1; i <= 512; i *= 2) { test( 'chunks randomly sized $i', () => callback(chunker(factory(), () => random.nextInt(1 + i))), ); } }); Stream stringChunker(String input, int Function() splitter) async* { while (input.isNotEmpty) { final size = min(splitter(), input.length); yield input.substring(0, size); input = input.substring(size); } } Stream> listChunker(List input, int Function() splitter) async* { while (input.isNotEmpty) { final size = min(splitter(), input.length); yield input.sublist(0, size); input = input.sublist(size); } } void main() { group('events', () { for (final entry in allXml.entries) { group(entry.key, () { late XmlDocument document; late String source; late List events; setUp(() { document = XmlDocument.parse(entry.value); source = document.toXmlString(); events = parseEvents(source).toList(growable: false); }); chunkedTests('string -> events', () => source, stringChunker, ( stream, ) { final actual = stream.toXmlEvents().normalizeEvents().flatten(); expect(actual, emitsInOrder([...events, emitsDone])); }); chunkedTests>( 'events -> nodes', () => events, listChunker, (stream) { final actual = stream.toXmlNodes().flatten(); final expected = document.children.map( (node) => predicate((actual) { compareNode(actual, node); return true; }, 'matches $node'), ); expect(actual, emitsInOrder([...expected, emitsDone])); }, ); chunkedTests>( 'nodes -> events', () => document.children, listChunker, (stream) { final actual = stream.toXmlEvents().flatten(); expect(actual, emitsInOrder([...events, emitsDone])); }, ); chunkedTests>( 'events -> string', () => events, listChunker, (stream) { final actual = stream.toXmlString().join(); expect(actual, completion(source)); }, ); chunkedTests( 'string -> events -> string', () => source, stringChunker, (stream) { final actual = stream .toXmlEvents() .normalizeEvents() .toXmlString() .join(); expect(actual, completion(source)); }, ); chunkedTests>( 'events -> string -> events', () => events, listChunker, (stream) { final actual = stream .toXmlString() .toXmlEvents() .normalizeEvents() .flatten() .toList(); expect(actual, completion(events)); }, ); chunkedTests>( 'events -> nodes -> events', () => events, listChunker, (stream) { final actual = stream.toXmlNodes().toXmlEvents().flatten().toList(); expect(actual, completion(events)); }, ); chunkedTests>( 'nodes -> events -> nodes', () => document.children, listChunker, (stream) async { final actual = await stream .toXmlEvents() .toXmlNodes() .flatten() .toList(); expect( actual, pairwiseCompare(document.children, ( actual, expected, ) { compareNode(actual, expected); return true; }, 'not matching'), ); }, ); if (entry.value == shiporderXsd) { chunkedTests>( 'events -> subtree -> nodes', () => events, listChunker, (stream) async { final actual = await stream .selectSubtreeEvents((event) => event.name == 'xsd:element') .toXmlNodes() .flatten() .toList(); final expected = document .findAllElements('element', namespace: '*') .where( (element) => !element.ancestors.whereType().any( (parent) => parent.name.local == 'element', ), ) .toList(); expect( actual, pairwiseCompare(expected, (actual, expected) { compareNode(actual, expected); return true; }, 'not matching'), ); actual .expand((node) => [node, ...node.descendants]) .whereType() .forEach((node) => expect(node.name.namespaceUri, isNull)); }, ); chunkedTests>( 'events -> parents -> subtree -> nodes', () => events, listChunker, (stream) async { final actual = await stream .withParentEvents() .selectSubtreeEvents((event) => event.name == 'xsd:element') .toXmlNodes() .flatten() .toList(); final expected = document .findAllElements('element', namespace: '*') .where( (element) => !element.ancestors.whereType().any( (parent) => parent.name.local == 'element', ), ) .toList(); expect( actual, pairwiseCompare(expected, (actual, expected) { compareNode(actual, expected); return true; }, 'not matching'), ); actual .expand((node) => [node, ...node.descendants]) .whereType() .where((node) => node.name.prefix == 'xsd') .forEach( (node) => expect( node.name.namespaceUri, 'http://www.w3.org/2001/XMLSchema', ), ); }, ); } chunkedTests>( 'event -> forEachEvent', () => events, listChunker, (stream) async { final cdata = []; final comment = []; final declaration = []; final doctype = []; final endElement = []; final processing = []; final startElement = []; final text = []; await stream.flatten().forEachEvent( onCDATA: cdata.add, onComment: comment.add, onDeclaration: declaration.add, onDoctype: doctype.add, onEndElement: endElement.add, onProcessing: processing.add, onStartElement: startElement.add, onText: text.add, ); expect(cdata, events.whereType()); expect(comment, events.whereType()); expect(declaration, events.whereType()); expect(doctype, events.whereType()); expect(endElement, events.whereType()); expect(processing, events.whereType()); expect(startElement, events.whereType()); expect(text, events.whereType()); }, ); chunkedTests>( 'events -> tapEachEvent', () => events, listChunker, (stream) async { final cdata = []; final comment = []; final declaration = []; final doctype = []; final endElement = []; final processing = []; final startElement = []; final text = []; await stream .flatten() .tapEachEvent( onCDATA: cdata.add, onComment: comment.add, onDeclaration: declaration.add, onDoctype: doctype.add, onEndElement: endElement.add, onProcessing: processing.add, onStartElement: startElement.add, onText: text.add, ) .drain(); expect(cdata, events.whereType()); expect(comment, events.whereType()); expect(declaration, events.whereType()); expect(doctype, events.whereType()); expect(endElement, events.whereType()); expect(processing, events.whereType()); expect(startElement, events.whereType()); expect(text, events.whereType()); }, ); chunkedTests>( 'events -> forEachEvent', () => events, listChunker, (stream) async { final cdata = []; final comment = []; final declaration = []; final doctype = []; final endElement = []; final processing = []; final startElement = []; final text = []; await stream.forEachEvent( onCDATA: cdata.add, onComment: comment.add, onDeclaration: declaration.add, onDoctype: doctype.add, onEndElement: endElement.add, onProcessing: processing.add, onStartElement: startElement.add, onText: text.add, ); expect(cdata, events.whereType()); expect(comment, events.whereType()); expect(declaration, events.whereType()); expect(doctype, events.whereType()); expect(endElement, events.whereType()); expect(processing, events.whereType()); expect(startElement, events.whereType()); expect(text, events.whereType()); }, ); chunkedTests>( 'events -> tapEachEvent', () => events, listChunker, (stream) async { final cdata = []; final comment = []; final declaration = []; final doctype = []; final endElement = []; final processing = []; final startElement = []; final text = []; await stream .tapEachEvent( onCDATA: cdata.add, onComment: comment.add, onDeclaration: declaration.add, onDoctype: doctype.add, onEndElement: endElement.add, onProcessing: processing.add, onStartElement: startElement.add, onText: text.add, ) .drain(); expect(cdata, events.whereType()); expect(comment, events.whereType()); expect(declaration, events.whereType()); expect(doctype, events.whereType()); expect(endElement, events.whereType()); expect(processing, events.whereType()); expect(startElement, events.whereType()); expect(text, events.whereType()); }, ); chunkedTests>( 'events -> withParent -> map', () => events, listChunker, (stream) async { final stacks = await stream .withParentEvents() .normalizeEvents() .flatten() .map((event) { final stack = []; for ( XmlEvent? current = event; current != null; current = current.parent ) { stack.insert(0, current); } return stack; }) .toList(); expect(stacks.map((events) => events.last), events); }, ); }); } }); group('errors', () { chunkedTests('missing tag closing', () => '" expected', position: 6), ), ), ); }); chunkedTests( 'missing attribute closing', () => '" expected', position: 8), ), ), ); }, ); chunkedTests( 'missing comment closing', () => '" expected', position: 4), ), ), ); }, ); group('tags not validated', () { chunkedTests( 'unexpected end tag', () => '', stringChunker, (stream) { expect( stream.toXmlEvents(withLocation: true).flatten(), emitsInOrder([XmlEndElementEvent('foo'), emitsDone]), ); }, ); chunkedTests('missing end tag', () => '', stringChunker, ( stream, ) { expect( stream.toXmlEvents(withLocation: true).flatten(), emitsInOrder([XmlStartElementEvent('foo', [], false), emitsDone]), ); }); chunkedTests( 'not matching end tag', () => '', stringChunker, (stream) { expect( stream.toXmlEvents(withLocation: true).flatten(), emitsInOrder([ XmlStartElementEvent('foo', [], false), XmlEndElementEvent('bar'), XmlEndElementEvent('foo'), emitsDone, ]), ); }, ); }); group('tags validated', () { chunkedTests( 'unexpected end tag', () => '', stringChunker, (stream) { expect( stream .toXmlEvents(validateNesting: true, withLocation: true) .flatten(), emitsThrough( emitsError(isXmlTagException(actualName: 'foo', position: 0)), ), ); }, ); chunkedTests('missing end tag', () => '', stringChunker, ( stream, ) { expect( stream .toXmlEvents(validateNesting: true, withLocation: true) .flatten(), emitsThrough( emitsError(isXmlTagException(expectedName: 'foo', position: 5)), ), ); }); chunkedTests( 'not matching end tag', () => '', stringChunker, (stream) { expect( stream .toXmlEvents(validateNesting: true, withLocation: true) .flatten(), emitsThrough( emitsError( isXmlTagException( expectedName: 'foo', actualName: 'bar', position: 5, ), ), ), ); }, ); }); group('node decoder', () { const decoder = XmlNodeDecoder(); test('mismatch closing tag', () { expect( () => decoder.convert([ XmlStartElementEvent('foo', [], false), XmlEndElementEvent('bar'), ]), throwsA( isXmlTagException( message: 'Expected , but found ', expectedName: 'foo', actualName: 'bar', ), ), ); }); test('unexpected closing tag', () { expect( () => decoder.convert([XmlEndElementEvent('foo')]), throwsA( isXmlTagException( message: 'Unexpected ', expectedName: isNull, actualName: 'foo', ), ), ); }); test('missing closing tag', () { expect( () => decoder.convert([XmlStartElementEvent('foo', [], false)]), throwsA( isXmlTagException( message: 'Missing ', expectedName: 'foo', actualName: isNull, ), ), ); }); test('hidden parents', () { final grandparent = XmlStartElementEvent('grandparent', [], false); final parent = XmlStartElementEvent('parent', [], false) ..attachParent(grandparent); final child = XmlStartElementEvent('child', [], true) ..attachParent(parent); final node = decoder.convert([child]).single; expect(node.hasParent, isTrue); expect(node.parent?.hasParent, isTrue); expect( node.parent?.parent?.toXmlString(), '', ); }); }); }); group('normalizeEvents', () { test('empty', () async { final input = [XmlTextEvent('')]; final output = await Stream.fromIterable([ input, ]).normalizeEvents().flatten().toList(); const expected = []; expect(output, expected); }); test('whitespace', () async { final input = [XmlTextEvent(' \n\t')]; final actual = await Stream.fromIterable([ input, ]).normalizeEvents().flatten().toList(); final expected = [XmlTextEvent(' \n\t')]; expect(actual, expected); }); test('combine two', () async { final input = [XmlTextEvent('a'), XmlTextEvent('b')]; final actual = await Stream.fromIterable([ input, ]).normalizeEvents().flatten().toList(); final expected = [XmlTextEvent('ab')]; expect(actual, expected); }); test('combine many', () async { final input = [ XmlTextEvent('a'), XmlTextEvent('b'), XmlTextEvent('c'), XmlTextEvent('d'), XmlTextEvent('e'), ]; final actual = await Stream.fromIterable([ input, ]).normalizeEvents().flatten().toList(); final expected = [XmlTextEvent('abcde')]; expect(actual, expected); }); test('chunked up', () async { final input = [ XmlTextEvent('a'), XmlTextEvent('b'), XmlTextEvent('c'), XmlStartElementEvent('br', [], true), XmlTextEvent('d'), XmlTextEvent('e'), ]; final actual = await Stream.fromIterable([ input, ]).normalizeEvents().flatten().toList(); final expected = [ XmlTextEvent('abc'), XmlStartElementEvent('br', [], true), XmlTextEvent('de'), ]; expect(actual, expected); }); }); group('withParentEvents', () { test('not parented', () async { final input = [ XmlCDATAEvent('cdata'), XmlCommentEvent('comment'), XmlDeclarationEvent([]), XmlDoctypeEvent('doctype'), XmlProcessingEvent('target', 'text'), XmlStartElementEvent('element', [], true), XmlTextEvent('text'), ]; final output = await Stream.fromIterable([ input, ]).withParentEvents().flatten().toList(); expect(output, input, reason: 'equality is unaffected'); for (var i = 0; i < input.length; i++) { expect(input[i], same(output[i]), reason: 'root element is identical'); } }); test('basic parented', () async { final input = [ XmlStartElementEvent('element', [], false), XmlCDATAEvent('cdata'), XmlCommentEvent('comment'), XmlDeclarationEvent([]), XmlDoctypeEvent('doctype'), XmlProcessingEvent('target', 'text'), XmlStartElementEvent('element', [], true), XmlTextEvent('text'), XmlEndElementEvent('element'), ]; final output = await Stream.fromIterable([ input, ]).withParentEvents().flatten().toList(); expect(output, input, reason: 'equality is unaffected'); for (var i = 1; i < input.length; i++) { expect(output[i].parent, same(output[0])); expect(output[i].parent, same(input[0])); } }); test('deeply parented', () async { final input = [ XmlStartElementEvent('first', [], false), XmlStartElementEvent('second', [], false), XmlStartElementEvent('third', [], false), XmlEndElementEvent('third'), XmlEndElementEvent('second'), XmlEndElementEvent('first'), ]; final output = await Stream.fromIterable([ input, ]).withParentEvents().flatten().toList(); expect(output, input, reason: 'equality is unaffected'); expect(output[0], same(input[0]), reason: 'root element is identical'); expect(output[0].parent, isNull); expect(output[1].parent, same(output[0])); expect(output[2].parent, same(output[1])); expect(output[3].parent, same(output[2])); expect(output[4].parent, same(output[1])); expect(output[5].parent, same(output[0])); }); test('closing tag mismatch', () { final input = >[ [XmlStartElementEvent('open', [], false)], [XmlEndElementEvent('close')], [XmlTextEvent('after')], ]; final stream = Stream.fromIterable(input).withParentEvents().flatten(); expect( stream, emitsInOrder([ input[0][0], emitsError( isXmlTagException(message: 'Expected , but found '), ), ]), ); }); test('closing tag missing', () { final input = >[ [XmlStartElementEvent('open', [], false)], ]; final stream = Stream.fromIterable(input).withParentEvents().flatten(); expect( stream, emitsInOrder([ input[0][0], emitsError(isXmlTagException(message: 'Missing ')), ]), ); }); test('closing tag unexpected', () { final input = >[ [XmlEndElementEvent('close')], [XmlTextEvent('after')], ]; final stream = Stream.fromIterable(input).withParentEvents().flatten(); expect( stream, emitsError(isXmlTagException(message: 'Unexpected ')), ); }); test('after normalization', () { final input = [ XmlStartElementEvent('outer', [], false), XmlTextEvent('first'), XmlTextEvent(' '), XmlTextEvent('second'), XmlEndElementEvent('outer'), ]; final actual = const XmlWithParentEvents().convert( const XmlNormalizeEvents().convert(input), ); expect(actual, hasLength(3)); expect(actual[1].parent, same(actual[0])); expect(actual[2].parent, same(actual[0])); }); test('before normalization', () { final input = [ XmlStartElementEvent('outer', [], false), XmlTextEvent('first'), XmlTextEvent(' '), XmlTextEvent('second'), XmlEndElementEvent('outer'), ]; final actual = const XmlNormalizeEvents().convert( const XmlWithParentEvents().convert(input), ); expect(actual, hasLength(3)); expect(actual[1].parent, same(actual[0])); expect(actual[2].parent, same(actual[0])); }); test('default namespace', () async { const url = 'http://www.w3.org/1999/xhtml'; const input = ''; final events = await Stream.fromIterable([ input, ]).toXmlEvents().withParentEvents().flatten().toList(); for (final event in events) { if (event is XmlStartElementEvent) { expect(event.namespaceUri, url); event.attributes .where((attribute) => attribute.localName != 'xmlns') .forEach((attribute) => expect(attribute.namespaceUri, url)); } else if (event is XmlEndElementEvent) { expect(event.namespaceUri, url); } } }); test('prefix namespace', () async { const url = 'http://www.w3.org/1999/xhtml'; const input = '' '' ''; final events = await Stream.fromIterable([ input, ]).toXmlEvents().withParentEvents().flatten().toList(); for (final event in events) { if (event is XmlStartElementEvent) { expect(event.namespaceUri, url); event.attributes .where((attribute) => attribute.namespacePrefix != 'xmlns') .forEach((attribute) => expect(attribute.namespaceUri, url)); } else if (event is XmlEndElementEvent) { expect(event.namespaceUri, url); } } }); }); }