| // Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "tools/gn/operators.h" | |
| #include <stdint.h> | |
| #include <utility> | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "tools/gn/parse_tree.h" | |
| #include "tools/gn/pattern.h" | |
| #include "tools/gn/test_with_scope.h" | |
| namespace { | |
| bool IsValueIntegerEqualing(const Value& v, int64_t i) { | |
| if (v.type() != Value::INTEGER) | |
| return false; | |
| return v.int_value() == i; | |
| } | |
| bool IsValueStringEqualing(const Value& v, const char* s) { | |
| if (v.type() != Value::STRING) | |
| return false; | |
| return v.string_value() == s; | |
| } | |
| // This parse node is for passing to tests. It returns a canned value for | |
| // Execute(). | |
| class TestParseNode : public ParseNode { | |
| public: | |
| TestParseNode(const Value& v) : value_(v) { | |
| } | |
| Value Execute(Scope* scope, Err* err) const override { | |
| return value_; | |
| } | |
| LocationRange GetRange() const override { | |
| return LocationRange(); | |
| } | |
| Err MakeErrorDescribing(const std::string& msg, | |
| const std::string& help) const override { | |
| return Err(this, msg); | |
| } | |
| void Print(std::ostream& out, int indent) const override { | |
| } | |
| private: | |
| Value value_; | |
| }; | |
| // Sets up a BinaryOpNode for testing. | |
| class TestBinaryOpNode : public BinaryOpNode { | |
| public: | |
| // Input token value string must outlive class. | |
| TestBinaryOpNode(Token::Type op_token_type, | |
| const char* op_token_value) | |
| : BinaryOpNode(), | |
| op_token_ownership_(Location(), op_token_type, op_token_value) { | |
| set_op(op_token_ownership_); | |
| } | |
| void SetLeftToValue(const Value& value) { | |
| set_left(std::unique_ptr<ParseNode>(new TestParseNode(value))); | |
| } | |
| // Sets the left-hand side of the operator to an identifier node, this is | |
| // used for testing assignments. Input string must outlive class. | |
| void SetLeftToIdentifier(const char* identifier) { | |
| left_identifier_token_ownership_ = | |
| Token(Location(), Token::IDENTIFIER, identifier); | |
| set_left(std::unique_ptr<ParseNode>( | |
| new IdentifierNode(left_identifier_token_ownership_))); | |
| } | |
| void SetRightToValue(const Value& value) { | |
| set_right(std::unique_ptr<ParseNode>(new TestParseNode(value))); | |
| } | |
| void SetRightToListOfValue(const Value& value) { | |
| Value list(nullptr, Value::LIST); | |
| list.list_value().push_back(value); | |
| set_right(std::unique_ptr<ParseNode>(new TestParseNode(list))); | |
| } | |
| void SetRightToListOfValue(const Value& value1, const Value& value2) { | |
| Value list(nullptr, Value::LIST); | |
| list.list_value().push_back(value1); | |
| list.list_value().push_back(value2); | |
| set_right(std::unique_ptr<ParseNode>(new TestParseNode(list))); | |
| } | |
| private: | |
| // The base class takes the Token by reference, this manages the lifetime. | |
| Token op_token_ownership_; | |
| // When setting the left to an identifier, this manages the lifetime of | |
| // the identifier token. | |
| Token left_identifier_token_ownership_; | |
| }; | |
| } // namespace | |
| TEST(Operators, SourcesAppend) { | |
| Err err; | |
| TestWithScope setup; | |
| // Set up "sources" with an empty list. | |
| const char sources[] = "sources"; | |
| setup.scope()->SetValue(sources, Value(nullptr, Value::LIST), nullptr); | |
| // Set up the operator. | |
| TestBinaryOpNode node(Token::PLUS_EQUALS, "+="); | |
| node.SetLeftToIdentifier(sources); | |
| // Set up the filter on the scope to remove everything ending with "rm" | |
| std::unique_ptr<PatternList> pattern_list(new PatternList); | |
| pattern_list->Append(Pattern("*rm")); | |
| setup.scope()->set_sources_assignment_filter(std::move(pattern_list)); | |
| // Append an integer. | |
| node.SetRightToListOfValue(Value(nullptr, static_cast<int64_t>(5))); | |
| node.Execute(setup.scope(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| // Append a string that doesn't match the pattern, it should get appended. | |
| const char string1[] = "good"; | |
| node.SetRightToListOfValue(Value(nullptr, string1)); | |
| node.Execute(setup.scope(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| // Append a string that does match the pattern, it should be a no-op. | |
| const char string2[] = "foo-rm"; | |
| node.SetRightToListOfValue(Value(nullptr, string2)); | |
| node.Execute(setup.scope(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| // Append a list with the two strings from above. | |
| node.SetRightToListOfValue(Value(nullptr, string1), Value(nullptr, string2)); | |
| node.Execute(setup.scope(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| // The sources variable in the scope should now have: [ 5, "good", "good" ] | |
| const Value* value = setup.scope()->GetValue(sources); | |
| ASSERT_TRUE(value); | |
| ASSERT_EQ(Value::LIST, value->type()); | |
| ASSERT_EQ(3u, value->list_value().size()); | |
| EXPECT_TRUE(IsValueIntegerEqualing(value->list_value()[0], 5)); | |
| EXPECT_TRUE(IsValueStringEqualing(value->list_value()[1], "good")); | |
| EXPECT_TRUE(IsValueStringEqualing(value->list_value()[2], "good")); | |
| } | |
| // Note that the SourcesAppend test above tests the basic list + list features, | |
| // this test handles the other cases. | |
| TEST(Operators, ListAppend) { | |
| Err err; | |
| TestWithScope setup; | |
| // Set up "foo" with an empty list. | |
| const char foo[] = "foo"; | |
| setup.scope()->SetValue(foo, Value(nullptr, Value::LIST), nullptr); | |
| // Set up the operator to append to "foo". | |
| TestBinaryOpNode node(Token::PLUS_EQUALS, "+="); | |
| node.SetLeftToIdentifier(foo); | |
| // Append a list with a list, the result should be a nested list. | |
| Value inner_list(nullptr, Value::LIST); | |
| inner_list.list_value().push_back(Value(nullptr, static_cast<int64_t>(12))); | |
| node.SetRightToListOfValue(inner_list); | |
| Value ret = ExecuteBinaryOperator(setup.scope(), &node, node.left(), | |
| node.right(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| // Return from the operator should always be "none", it should update the | |
| // value only. | |
| EXPECT_EQ(Value::NONE, ret.type()); | |
| // The value should be updated with "[ [ 12 ] ]" | |
| Value result = *setup.scope()->GetValue(foo); | |
| ASSERT_EQ(Value::LIST, result.type()); | |
| ASSERT_EQ(1u, result.list_value().size()); | |
| ASSERT_EQ(Value::LIST, result.list_value()[0].type()); | |
| ASSERT_EQ(1u, result.list_value()[0].list_value().size()); | |
| ASSERT_EQ(Value::INTEGER, result.list_value()[0].list_value()[0].type()); | |
| ASSERT_EQ(12, result.list_value()[0].list_value()[0].int_value()); | |
| // Try to append an integer and a string directly (e.g. foo += "hi"). | |
| // This should fail. | |
| const char str_str[] = "\"hi\""; | |
| Token str(Location(), Token::STRING, str_str); | |
| node.set_right(std::unique_ptr<ParseNode>(new LiteralNode(str))); | |
| ExecuteBinaryOperator(setup.scope(), &node, node.left(), node.right(), &err); | |
| EXPECT_TRUE(err.has_error()); | |
| err = Err(); | |
| node.SetRightToValue(Value(nullptr, static_cast<int64_t>(12))); | |
| ExecuteBinaryOperator(setup.scope(), &node, node.left(), node.right(), &err); | |
| EXPECT_TRUE(err.has_error()); | |
| } | |
| TEST(Operators, ListRemove) { | |
| Err err; | |
| TestWithScope setup; | |
| const char foo_str[] = "foo"; | |
| const char bar_str[] = "bar"; | |
| Value test_list(nullptr, Value::LIST); | |
| test_list.list_value().push_back(Value(nullptr, foo_str)); | |
| test_list.list_value().push_back(Value(nullptr, bar_str)); | |
| test_list.list_value().push_back(Value(nullptr, foo_str)); | |
| // Set up "var" with an the test list. | |
| const char var[] = "var"; | |
| setup.scope()->SetValue(var, test_list, nullptr); | |
| TestBinaryOpNode node(Token::MINUS_EQUALS, "-="); | |
| node.SetLeftToIdentifier(var); | |
| // Subtract a list consisting of "foo". | |
| node.SetRightToListOfValue(Value(nullptr, foo_str)); | |
| Value result = ExecuteBinaryOperator( | |
| setup.scope(), &node, node.left(), node.right(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| // -= returns an empty value to reduce the possibility of writing confusing | |
| // cases like foo = bar += 1. | |
| EXPECT_EQ(Value::NONE, result.type()); | |
| // The "var" variable should have been updated. Both instances of "foo" are | |
| // deleted. | |
| const Value* new_value = setup.scope()->GetValue(var); | |
| ASSERT_TRUE(new_value); | |
| ASSERT_EQ(Value::LIST, new_value->type()); | |
| ASSERT_EQ(1u, new_value->list_value().size()); | |
| ASSERT_EQ(Value::STRING, new_value->list_value()[0].type()); | |
| EXPECT_EQ("bar", new_value->list_value()[0].string_value()); | |
| } | |
| TEST(Operators, IntegerAdd) { | |
| Err err; | |
| TestWithScope setup; | |
| TestBinaryOpNode node(Token::PLUS, "+"); | |
| node.SetLeftToValue(Value(nullptr, static_cast<int64_t>(123))); | |
| node.SetRightToValue(Value(nullptr, static_cast<int64_t>(456))); | |
| Value ret = ExecuteBinaryOperator(setup.scope(), &node, node.left(), | |
| node.right(), &err); | |
| ASSERT_FALSE(err.has_error()); | |
| ASSERT_EQ(Value::INTEGER, ret.type()); | |
| EXPECT_EQ(579, ret.int_value()); | |
| } | |
| TEST(Operators, IntegerSubtract) { | |
| Err err; | |
| TestWithScope setup; | |
| TestBinaryOpNode node(Token::MINUS, "-"); | |
| node.SetLeftToValue(Value(nullptr, static_cast<int64_t>(123))); | |
| node.SetRightToValue(Value(nullptr, static_cast<int64_t>(456))); | |
| Value ret = ExecuteBinaryOperator(setup.scope(), &node, node.left(), | |
| node.right(), &err); | |
| ASSERT_FALSE(err.has_error()); | |
| ASSERT_EQ(Value::INTEGER, ret.type()); | |
| EXPECT_EQ(-333, ret.int_value()); | |
| } | |
| TEST(Operators, ShortCircuitAnd) { | |
| Err err; | |
| TestWithScope setup; | |
| // Set a && operator with the left to false. | |
| TestBinaryOpNode node(Token::BOOLEAN_AND, "&&"); | |
| node.SetLeftToValue(Value(nullptr, false)); | |
| // Set right as foo, but don't define a value for it. | |
| const char foo[] = "foo"; | |
| Token identifier_token(Location(), Token::IDENTIFIER, foo); | |
| node.set_right( | |
| std::unique_ptr<ParseNode>(new IdentifierNode(identifier_token))); | |
| Value ret = ExecuteBinaryOperator(setup.scope(), &node, node.left(), | |
| node.right(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| } | |
| TEST(Operators, ShortCircuitOr) { | |
| Err err; | |
| TestWithScope setup; | |
| // Set a || operator with the left to true. | |
| TestBinaryOpNode node(Token::BOOLEAN_OR, "||"); | |
| node.SetLeftToValue(Value(nullptr, true)); | |
| // Set right as foo, but don't define a value for it. | |
| const char foo[] = "foo"; | |
| Token identifier_token(Location(), Token::IDENTIFIER, foo); | |
| node.set_right( | |
| std::unique_ptr<ParseNode>(new IdentifierNode(identifier_token))); | |
| Value ret = ExecuteBinaryOperator(setup.scope(), &node, node.left(), | |
| node.right(), &err); | |
| EXPECT_FALSE(err.has_error()); | |
| } | |
| // Overwriting nonempty lists and scopes with other nonempty lists and scopes | |
| // should be disallowed. | |
| TEST(Operators, NonemptyOverwriting) { | |
| Err err; | |
| TestWithScope setup; | |
| // Set up "foo" with a nonempty list. | |
| const char foo[] = "foo"; | |
| Value old_value(nullptr, Value::LIST); | |
| old_value.list_value().push_back(Value(nullptr, "string")); | |
| setup.scope()->SetValue(foo, old_value, nullptr); | |
| TestBinaryOpNode node(Token::EQUAL, "="); | |
| node.SetLeftToIdentifier(foo); | |
| // Assigning a nonempty list should fail. | |
| node.SetRightToListOfValue(Value(nullptr, "string")); | |
| node.Execute(setup.scope(), &err); | |
| ASSERT_TRUE(err.has_error()); | |
| EXPECT_EQ("Replacing nonempty list.", err.message()); | |
| err = Err(); | |
| // Assigning an empty list should succeed. | |
| node.SetRightToValue(Value(nullptr, Value::LIST)); | |
| node.Execute(setup.scope(), &err); | |
| ASSERT_FALSE(err.has_error()); | |
| const Value* new_value = setup.scope()->GetValue(foo); | |
| ASSERT_TRUE(new_value); | |
| ASSERT_EQ(Value::LIST, new_value->type()); | |
| ASSERT_TRUE(new_value->list_value().empty()); | |
| // Set up "foo" with a nonempty scope. | |
| const char bar[] = "bar"; | |
| old_value = | |
| Value(nullptr, std::unique_ptr<Scope>(new Scope(setup.settings()))); | |
| old_value.scope_value()->SetValue(bar, Value(nullptr, "bar"), nullptr); | |
| setup.scope()->SetValue(foo, old_value, nullptr); | |
| // Assigning a nonempty scope should fail (re-use old_value copy). | |
| node.SetRightToValue(old_value); | |
| node.Execute(setup.scope(), &err); | |
| ASSERT_TRUE(err.has_error()); | |
| EXPECT_EQ("Replacing nonempty scope.", err.message()); | |
| err = Err(); | |
| // Assigning an empty list should succeed. | |
| node.SetRightToValue( | |
| Value(nullptr, std::unique_ptr<Scope>(new Scope(setup.settings())))); | |
| node.Execute(setup.scope(), &err); | |
| ASSERT_FALSE(err.has_error()); | |
| new_value = setup.scope()->GetValue(foo); | |
| ASSERT_TRUE(new_value); | |
| ASSERT_EQ(Value::SCOPE, new_value->type()); | |
| ASSERT_FALSE(new_value->scope_value()->HasValues(Scope::SEARCH_CURRENT)); | |
| } | |
| // Tests this case: | |
| // foo = 1 | |
| // target(...) { | |
| // foo += 1 | |
| // | |
| // This should mark the outer "foo" as used, and the inner "foo" as unused. | |
| TEST(Operators, PlusEqualsUsed) { | |
| Err err; | |
| TestWithScope setup; | |
| // Outer "foo" definition, it should be unused. | |
| const char foo[] = "foo"; | |
| Value old_value(nullptr, static_cast<int64_t>(1)); | |
| setup.scope()->SetValue(foo, old_value, nullptr); | |
| EXPECT_TRUE(setup.scope()->IsSetButUnused(foo)); | |
| // Nested scope. | |
| Scope nested(setup.scope()); | |
| // Run "foo += 1". | |
| TestBinaryOpNode node(Token::PLUS_EQUALS, "+="); | |
| node.SetLeftToIdentifier(foo); | |
| node.SetRightToValue(Value(nullptr, static_cast<int64_t>(1))); | |
| node.Execute(&nested, &err); | |
| ASSERT_FALSE(err.has_error()); | |
| // Outer foo should be used, inner foo should not be. | |
| EXPECT_FALSE(setup.scope()->IsSetButUnused(foo)); | |
| EXPECT_TRUE(nested.IsSetButUnused(foo)); | |
| } |