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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

## Unreleased

- …
- Add BaseNode.equals for deep-equality testing.

Nodes are deeply compared on a field by field basis. If possible, False is
returned early. When comparing attributes, tags and variants in
SelectExpressions, the order doesn't matter. By default, spans are not
taken into account.

## fluent 0.4.0 (June 13th, 2017)

Expand Down
64 changes: 64 additions & 0 deletions fluent/syntax/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ def from_json(value):
return value


def scalars_equal(node1, node2, with_spans=False):
"""Compare two nodes which are not lists."""

if type(node1) != type(node2):
return False

if isinstance(node1, BaseNode):
return node1.equals(node2, with_spans)

return node1 == node2


class BaseNode(object):
"""Base class for all Fluent AST nodes.

Expand Down Expand Up @@ -61,6 +73,58 @@ def visit(value):

return fun(node)

def equals(self, other, with_spans=False):
"""Compare two nodes.

Nodes are deeply compared on a field by field basis. If possible, False
is returned early. When comparing attributes, tags and variants in
SelectExpressions, the order doesn't matter. By default, spans are not
taken into account.
"""

self_keys = set(vars(self).keys())
other_keys = set(vars(other).keys())

if not with_spans:
self_keys.discard('span')
other_keys.discard('span')

if self_keys != other_keys:
return False

for key in self_keys:
field1 = getattr(self, key)
field2 = getattr(other, key)

# List-typed nodes are compared item-by-item. When comparing
# attributes, tags and variants, the order of items doesn't matter.
if isinstance(field1, list) and isinstance(field2, list):
if len(field1) != len(field2):
return False

# These functions are used to sort lists of items for when
# order doesn't matter. Annotations are also lists but they
# can't be keyed on any of their fields reliably.
field_sorting = {
'attributes': lambda elem: elem.id.name,
'tags': lambda elem: elem.name.name,
'variants': lambda elem: elem.key.name,
}

if key in field_sorting:
sorting = field_sorting[key]
field1 = sorted(field1, key=sorting)
field2 = sorted(field2, key=sorting)

for elem1, elem2 in zip(field1, field2):
if not scalars_equal(elem1, elem2, with_spans):
return False

elif not scalars_equal(field1, field2, with_spans):
return False

return True

def to_json(self):
obj = {
name: to_json(value)
Expand Down
240 changes: 240 additions & 0 deletions tests/syntax/test_equals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
from __future__ import unicode_literals
import unittest
import sys

sys.path.append('.')

from tests.syntax import dedent_ftl
from fluent.syntax.parser import FluentParser


def identity(node):
return node


class TestEntryEqualToSelf(unittest.TestCase):
def setUp(self):
self.parser = FluentParser()

def parse_ftl_entry(self, string):
return self.parser.parse_entry(dedent_ftl(string))

def test_same_simple_message(self):
message1 = self.parse_ftl_entry("""\
foo = Foo
""")

self.assertTrue(message1.equals(message1))
self.assertTrue(message1.equals(message1.traverse(identity)))

def test_same_selector_message(self):
message1 = self.parse_ftl_entry("""\
foo =
{ $num ->
[one] One
[two] Two
[few] Few
[many] Many
*[other] Other
}
""")

self.assertTrue(message1.equals(message1))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to test for equality against the identity traversal, too?

If so, add that to the tests in this class?

self.assertTrue(message1.equals(message1.traverse(identity)))

def test_same_complex_placeable_message(self):
message1 = self.parse_ftl_entry("""\
foo = Foo { NUMBER($num, style: "decimal") } Bar
""")

self.assertTrue(message1.equals(message1))
self.assertTrue(message1.equals(message1.traverse(identity)))

def test_same_message_with_attribute(self):
message1 = self.parse_ftl_entry("""\
foo
.attr = Attr
""")

self.assertTrue(message1.equals(message1))
self.assertTrue(message1.equals(message1.traverse(identity)))

def test_same_message_with_attributes(self):
message1 = self.parse_ftl_entry("""\
foo
.attr1 = Attr 1
.attr2 = Attr 2
""")

self.assertTrue(message1.equals(message1))
self.assertTrue(message1.equals(message1.traverse(identity)))

def test_same_message_with_tag(self):
message1 = self.parse_ftl_entry("""\
foo = Foo
#tag
""")

self.assertTrue(message1.equals(message1))
self.assertTrue(message1.equals(message1.traverse(identity)))

def test_same_message_with_tags(self):
message1 = self.parse_ftl_entry("""\
foo = Foo
#tag1
#tag2
""")

self.assertTrue(message1.equals(message1))
self.assertTrue(message1.equals(message1.traverse(identity)))

def test_same_junk(self):
message1 = self.parse_ftl_entry("""\
foo = Foo {
""")

self.assertTrue(message1.equals(message1))
self.assertTrue(message1.equals(message1.traverse(identity)))


class TestOrderEquals(unittest.TestCase):
def setUp(self):
self.parser = FluentParser()

def parse_ftl_entry(self, string):
return self.parser.parse_entry(dedent_ftl(string))

def test_attributes(self):
message1 = self.parse_ftl_entry("""\
foo
.attr1 = Attr1
.attr2 = Attr2
""")
message2 = self.parse_ftl_entry("""\
foo
.attr2 = Attr2
.attr1 = Attr1
""")

self.assertTrue(message1.equals(message2))
self.assertTrue(message2.equals(message1))

def test_tags(self):
message1 = self.parse_ftl_entry("""\
foo = Foo
#tag1
#tag2
""")
message2 = self.parse_ftl_entry("""\
foo = Foo
#tag2
#tag1
""")

self.assertTrue(message1.equals(message2))
self.assertTrue(message2.equals(message1))

def test_variants(self):
message1 = self.parse_ftl_entry("""\
foo =
{ $num ->
[a] A
*[b] B
}
""")
message2 = self.parse_ftl_entry("""\
foo =
{ $num ->
*[b] B
[a] A
}
""")

self.assertTrue(message1.equals(message2))
self.assertTrue(message2.equals(message1))


class TestEqualWithSpans(unittest.TestCase):
def test_default_behavior(self):
parser = FluentParser()

strings = [
("foo = Foo", "foo = Foo"),
("foo = Foo", "foo = Foo"),
("foo = { $arg }", "foo = { $arg }"),
]

messages = [
(parser.parse_entry(a), parser.parse_entry(b))
for a, b in strings
]

for a, b in messages:
self.assertTrue(a.equals(b))

def test_parser_without_spans(self):
parser = FluentParser(with_spans=False)

strings = [
("foo = Foo", "foo = Foo"),
("foo = Foo", "foo = Foo"),
("foo = { $arg }", "foo = { $arg }"),
]

messages = [
(parser.parse_entry(a), parser.parse_entry(b))
for a, b in strings
]

for a, b in messages:
self.assertTrue(a.equals(b))

def test_equals_with_spans(self):
parser = FluentParser()

strings = [
("foo = Foo", "foo = Foo"),
("foo = { $arg }", "foo = { $arg }"),
]

messages = [
(parser.parse_entry(a), parser.parse_entry(b))
for a, b in strings
]

for a, b in messages:
self.assertTrue(a.equals(b, with_spans=True))

def test_parser_without_spans_equals_with_spans(self):
parser = FluentParser(with_spans=False)

strings = [
("foo = Foo", "foo = Foo"),
("foo = Foo", "foo = Foo"),
("foo = { $arg }", "foo = { $arg }"),
("foo = { $arg }", "foo = { $arg }"),
]

messages = [
(parser.parse_entry(a), parser.parse_entry(b))
for a, b in strings
]

for a, b in messages:
self.assertTrue(a.equals(b, with_spans=True))

def test_differ_with_spans(self):
parser = FluentParser()

strings = [
("foo = Foo", "foo = Foo"),
("foo = { $arg }", "foo = { $arg }"),
]

messages = [
(parser.parse_entry(a), parser.parse_entry(b))
for a, b in strings
]

for a, b in messages:
self.assertFalse(a.equals(b, with_spans=True))