| // 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 <memory> |
| #include <utility> |
| |
| #include "tools/gn/parse_tree.h" |
| #include "tools/gn/pattern.h" |
| #include "tools/gn/test_with_scope.h" |
| #include "util/test/test.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::make_unique<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::make_unique<IdentifierNode>(left_identifier_token_ownership_)); |
| } |
| |
| void SetRightToValue(const Value& value) { |
| set_right(std::make_unique<TestParseNode>(value)); |
| } |
| void SetRightToListOfValue(const Value& value) { |
| Value list(nullptr, Value::LIST); |
| list.list_value().push_back(value); |
| set_right(std::make_unique<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::make_unique<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 = std::make_unique<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::make_unique<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, ListSubtractWithScope) { |
| Err err; |
| TestWithScope setup; |
| |
| Scope* scope_a = new Scope(setup.settings()); |
| Value scopeval_a(nullptr, std::unique_ptr<Scope>(scope_a)); |
| scope_a->SetValue("a", Value(nullptr, "foo"), nullptr); |
| |
| Scope* scope_b = new Scope(setup.settings()); |
| Value scopeval_b(nullptr, std::unique_ptr<Scope>(scope_b)); |
| scope_b->SetValue("b", Value(nullptr, "bar"), nullptr); |
| |
| Value lval(nullptr, Value::LIST); |
| lval.list_value().push_back(scopeval_a); |
| lval.list_value().push_back(scopeval_b); |
| |
| Scope* scope_a_other = new Scope(setup.settings()); |
| Value scopeval_a_other(nullptr, std::unique_ptr<Scope>(scope_a_other)); |
| scope_a_other->SetValue("a", Value(nullptr, "foo"), nullptr); |
| |
| Value rval(nullptr, Value::LIST); |
| rval.list_value().push_back(scopeval_a_other); |
| |
| TestBinaryOpNode node(Token::MINUS, "-"); |
| node.SetLeftToValue(lval); |
| node.SetRightToValue(rval); |
| Value ret = ExecuteBinaryOperator(setup.scope(), &node, node.left(), |
| node.right(), &err); |
| ASSERT_FALSE(err.has_error()); |
| ASSERT_EQ(Value::LIST, ret.type()); |
| |
| std::vector<Value> expected; |
| Scope* scope_expected = new Scope(setup.settings()); |
| Value scopeval_expected(nullptr, std::unique_ptr<Scope>(scope_expected)); |
| scope_expected->SetValue("b", Value(nullptr, "bar"), nullptr); |
| expected.push_back(scopeval_expected); |
| EXPECT_EQ(expected, ret.list_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::make_unique<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::make_unique<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::make_unique<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::make_unique<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)); |
| } |