// 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 #include #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(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( new IdentifierNode(left_identifier_token_ownership_))); } void SetRightToValue(const Value& value) { set_right(std::unique_ptr(new TestParseNode(value))); } void SetRightToListOfValue(const Value& value) { Value list(nullptr, Value::LIST); list.list_value().push_back(value); set_right(std::unique_ptr(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(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 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(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(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(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(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(123))); node.SetRightToValue(Value(nullptr, static_cast(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(123))); node.SetRightToValue(Value(nullptr, static_cast(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(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(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(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(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(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(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)); }