blob: db90ac9292695bd0c39873de9805e456592fa208 [file] [log] [blame]
// 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));
}