blob: 8d6650eaca699c6dbb21a47e1d696d85f94712bf [file] [log] [blame]
// Copyright 2017-2020 The Verible Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "common/formatting/line_wrap_searcher.h"
#include <vector>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/strings/match.h"
#include "common/formatting/basic_format_style.h"
#include "common/formatting/format_token.h"
#include "common/formatting/unwrapped_line.h"
#include "common/formatting/unwrapped_line_test_utils.h"
namespace verible {
namespace {
// This test class just binds a set of style parameters.
class SearchLineWrapsTestFixture : public UnwrappedLineMemoryHandler,
public ::testing::Test {
public:
SearchLineWrapsTestFixture() {
// Shorter column limit makes test case examples shorter.
style_.column_limit = 20;
style_.indentation_spaces = 3;
style_.wrap_spaces = 6;
style_.over_column_limit_penalty = 80;
}
int LevelsToSpaces(int levels) const {
return levels * style_.indentation_spaces;
}
FormattedExcerpt SearchLineWraps(const UnwrappedLine& uwline,
const BasicFormatStyle& style) {
// Bound the size of search for unit testing.
const auto results = verible::SearchLineWraps(uwline, style, 1000);
EXPECT_FALSE(results.empty());
EXPECT_TRUE(results.front().CompletedFormatting());
return results.front();
}
protected:
BasicFormatStyle style_;
};
// Test that wrap search works on an empty input.
TEST_F(SearchLineWrapsTestFixture, EmptyRange) {
const std::vector<TokenInfo> tokens;
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(0), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_TRUE(uwline_in.TokensRange().empty());
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_TRUE(formatted_line.Tokens().empty());
EXPECT_EQ(formatted_line.IndentationSpaces(), 0);
}
// Test that wrap search works on a single token.
TEST_F(SearchLineWrapsTestFixture, OneToken) {
// The enums of these tokens don't matter.
const std::vector<TokenInfo> tokens = {{0, "aaa"}};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(0), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 0;
ftokens_in[0].before.spaces_required = 99; // should be ignored
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), 1);
const auto& ftokens_out = formatted_line.Tokens();
// First token should never break.
EXPECT_EQ(ftokens_out.front().before.action, SpacingDecision::Append);
EXPECT_EQ(formatted_line.Render(), "aaa");
}
// Test that wrap search works on a single token, starting indented.
TEST_F(SearchLineWrapsTestFixture, OneTokenIndented) {
// The enums of these tokens don't matter.
const std::vector<TokenInfo> tokens = {{0, "bbbb"}};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(2), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 0;
ftokens_in[0].before.spaces_required = 77; // should be ignored
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), 1);
const auto& ftokens_out = formatted_line.Tokens();
// First token should never break.
EXPECT_EQ(ftokens_out.front().before.action, SpacingDecision::Append);
// 2 indentation levels, 3 spaces each = 6 spaces
EXPECT_EQ(formatted_line.Render(), " bbbb");
}
// Test that wrap search works tokens that fit on one line.
TEST_F(SearchLineWrapsTestFixture, FitsOnOneLine) {
const std::vector<TokenInfo> tokens = {
{0, "zz"},
{0, "yyy"},
{0, "xxxx"},
{0, "wwwww"},
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 77; // should be ignored
ftokens_in[1].before.break_penalty = 1;
ftokens_in[1].before.spaces_required = 1;
ftokens_in[2].before.break_penalty = 1;
ftokens_in[2].before.spaces_required = 1;
ftokens_in[3].before.break_penalty = 1;
ftokens_in[3].before.spaces_required = 1;
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), tokens.size());
const auto& ftokens_out = formatted_line.Tokens();
// Since all tokens fit on one line, expect no breaks.
EXPECT_EQ(ftokens_out[0].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[1].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[2].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[3].before.action, SpacingDecision::Append);
// Exactly 20 columns, which is the limit.
EXPECT_EQ(formatted_line.Render(), " zz yyy xxxx wwwww");
}
// Test that wrapping keeps formatted tokens under the column limit.
TEST_F(SearchLineWrapsTestFixture, WrapsToNextLine) {
const std::vector<TokenInfo> tokens = {
{0, "zz"},
{0, "yyy"},
{0, "xxxx"},
{0, "wwwwww"}, // One character more than the previous test case.
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 77; // should be ignored
ftokens_in[1].before.break_penalty = 1;
ftokens_in[1].before.spaces_required = 1;
ftokens_in[2].before.break_penalty = 1;
ftokens_in[2].before.spaces_required = 1;
ftokens_in[3].before.break_penalty = 1;
ftokens_in[3].before.spaces_required = 1;
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), tokens.size());
const auto& ftokens_out = formatted_line.Tokens();
// First token should never break.
EXPECT_EQ(ftokens_out[0].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[1].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[2].before.action, SpacingDecision::Append);
// Last token must wrap.
EXPECT_EQ(ftokens_out[3].before.action, SpacingDecision::Wrap);
// Would be 21 columns without wrapping.
EXPECT_EQ(formatted_line.Render(),
" zz yyy xxxx\n"
" wwwwww");
}
// Test that wrapping keeps formatted tokens under the column limit.
// Extended to test multiple wraps.
TEST_F(SearchLineWrapsTestFixture, WrapsToNextLineMultiple) {
const std::vector<TokenInfo> tokens = {
{0, "zz"},
{0, "yyy"},
{0, "xxxx"},
{0, "wwwwww"}, // Expect this one to wrap (from previous test case)
{0, "a"},
{0, "1"},
{0, "2"}, // Expect to break before this token.
{0, "3"},
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 33; // should be ignored
for (size_t i = 1; i < tokens.size(); ++i) {
ftokens_in[i].before.break_penalty = 1;
ftokens_in[i].before.spaces_required = 1;
}
// forced tie-breaker among otherwise equally good formattings:
ftokens_in[5].before.break_penalty = 2;
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), tokens.size());
const auto& ftokens_out = formatted_line.Tokens();
EXPECT_EQ(ftokens_out[0].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[1].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[2].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[3].before.action, SpacingDecision::Wrap);
EXPECT_EQ(ftokens_out[4].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[5].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[6].before.action, SpacingDecision::Wrap);
EXPECT_EQ(ftokens_out[7].before.action, SpacingDecision::Append);
EXPECT_EQ(formatted_line.Render(),
" zz yyy xxxx\n"
" wwwwww a 1\n"
" 2 3");
}
// Test that wrapping keeps formatted tokens under the column limit.
// Testing different before.spaces_required.
TEST_F(SearchLineWrapsTestFixture, WrapsToNextLineMultipleDifferentSpaces) {
const std::vector<TokenInfo> tokens = {
{0, "zz"}, {0, "==="}, {0, "xxxx"},
{0, "wwwwww"}, {0, "a"}, // Expect to break before this token.
{0, "1"}, {0, "2"}, {0, "3"},
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 33; // should be ignored
ftokens_in[1].before.break_penalty = 1;
ftokens_in[1].before.spaces_required = 0;
ftokens_in[2].before.break_penalty = 1;
ftokens_in[2].before.spaces_required = 0;
for (size_t i = 3; i < tokens.size(); ++i) {
ftokens_in[i].before.break_penalty = 1;
ftokens_in[i].before.spaces_required = 2; // more spacing
}
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), tokens.size());
const auto& ftokens_out = formatted_line.Tokens();
EXPECT_EQ(ftokens_out[0].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[1].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[2].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[3].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[4].before.action, SpacingDecision::Wrap);
EXPECT_EQ(ftokens_out[5].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[6].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[7].before.action, SpacingDecision::Append);
EXPECT_EQ(formatted_line.Render(),
" zz===xxxx wwwwww\n"
" a 1 2 3");
}
// Test that wrapping search honors SpacingOptions::No as a constraint.
TEST_F(SearchLineWrapsTestFixture, ForcedJoins) {
const std::vector<TokenInfo> tokens = {
{0, "aaaaaa"},
{0, "bbbbb"}, // Normally this would fit on first line, but being forced
{0, "ccccc"}, // to adhere to this token, it must break earlier.
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 11; // should be ignored
ftokens_in[1].before.break_penalty = 1;
ftokens_in[1].before.spaces_required = 1;
ftokens_in[2].before.break_penalty = 1;
ftokens_in[2].before.spaces_required = 1;
ftokens_in[2].before.break_decision =
SpacingOptions::MustAppend; // This causes search to break earlier.
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), tokens.size());
const auto& ftokens_out = formatted_line.Tokens();
EXPECT_EQ(ftokens_out[0].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[1].before.action, SpacingDecision::Wrap);
EXPECT_EQ(ftokens_out[2].before.action, SpacingDecision::Append);
EXPECT_EQ(formatted_line.Render(),
" aaaaaa\n"
" bbbbb ccccc");
}
// Test that wrapping search honors SpacingOptions::MustWrap as a constraint.
TEST_F(SearchLineWrapsTestFixture, ForcedWraps) {
const std::vector<TokenInfo> tokens = {
{0, "aaaaaa"},
{0, "bbbbb"}, // Force a break here.
{0, "ccccc"},
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 11; // should be ignored
ftokens_in[1].before.break_penalty = 1;
ftokens_in[1].before.spaces_required = 1;
ftokens_in[1].before.break_decision = SpacingOptions::MustWrap;
ftokens_in[2].before.break_penalty = 1;
ftokens_in[2].before.spaces_required = 1;
const FormattedExcerpt formatted_line = SearchLineWraps(uwline_in, style_);
EXPECT_EQ(formatted_line.Tokens().size(), tokens.size());
const auto& ftokens_out = formatted_line.Tokens();
EXPECT_EQ(ftokens_out[0].before.action, SpacingDecision::Append);
EXPECT_EQ(ftokens_out[1].before.action, SpacingDecision::Wrap);
EXPECT_EQ(ftokens_out[2].before.action, SpacingDecision::Append);
EXPECT_EQ(formatted_line.Render(),
" aaaaaa\n" // Force break before 'bbbbb'
" bbbbb ccccc");
}
// Test multiple equally good wrapping solutions can be found and diagnosed.
TEST_F(SearchLineWrapsTestFixture, DisplayEquallyOptimalWrappings) {
const std::vector<TokenInfo> tokens = {
{0, "aaaaaaaaaa"},
{0, "bbbbb"},
{0, "ccccc"},
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 11; // should be ignored
ftokens_in[1].before.break_penalty = 3;
ftokens_in[1].before.spaces_required = 1;
ftokens_in[2].before.break_penalty = ftokens_in[1].before.break_penalty;
ftokens_in[2].before.spaces_required = 1;
const auto formatted_lines = verible::SearchLineWraps(uwline_in, style_, 10);
EXPECT_EQ(formatted_lines.size(), 1);
// By total cost alone, expected solutions are:
// break before token[1] and
// break before token[2].
// However, using a tie-breaker like terminal-column position, will favor
// equally good solutions that break earlier.
const auto& first = formatted_lines.front();
EXPECT_EQ(first.Tokens()[1].before.action, SpacingDecision::Append);
EXPECT_EQ(first.Tokens()[2].before.action, SpacingDecision::Wrap);
std::ostringstream stream;
DisplayEquallyOptimalWrappings(stream, uwline_in, formatted_lines);
// Limited output checking.
EXPECT_TRUE(absl::StrContains(stream.str(), "Found 1 equally good"));
EXPECT_TRUE(absl::StrContains(stream.str(), "============"));
}
TEST_F(SearchLineWrapsTestFixture, FitsOnLine) {
const std::vector<TokenInfo> tokens = {
{0, "aaaaaa"},
{0, "bbbbb"},
{0, "ccccc"},
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(0), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.spaces_required = 99; // irrelevant
ftokens_in[1].before.spaces_required = 1;
ftokens_in[2].before.spaces_required = 1;
EXPECT_TRUE(FitsOnLine(uwline_in, style_).fits);
EXPECT_EQ(FitsOnLine(uwline_in, style_).final_column, 18);
uwline_in.SetIndentationSpaces(2);
// fits: 2 + 6 + 1 + 5 + 1 + 5 = 20
EXPECT_TRUE(FitsOnLine(uwline_in, style_).fits);
EXPECT_EQ(FitsOnLine(uwline_in, style_).final_column, 20);
uwline_in.SetIndentationSpaces(3);
// not fits: 3 + 6 + 1 + 5 + 1 + 5 = 21
EXPECT_FALSE(FitsOnLine(uwline_in, style_).fits);
EXPECT_EQ(FitsOnLine(uwline_in, style_).final_column, 21);
uwline_in.SetIndentationSpaces(2);
ftokens_in[1].before.spaces_required = 2;
// not fits: 2 + 6 + 2 + 5 + 1 + 5 = 21
EXPECT_FALSE(FitsOnLine(uwline_in, style_).fits);
EXPECT_EQ(FitsOnLine(uwline_in, style_).final_column, 21);
ftokens_in[1].before.spaces_required = 1;
// fits: 2 + 6 + 1 + 5 + 1 + 5 = 20
EXPECT_TRUE(FitsOnLine(uwline_in, style_).fits);
EXPECT_EQ(FitsOnLine(uwline_in, style_).final_column, 20);
ftokens_in[2].before.break_decision =
SpacingOptions::MustWrap; // forced break
EXPECT_FALSE(FitsOnLine(uwline_in, style_).fits);
EXPECT_EQ(FitsOnLine(uwline_in, style_).final_column, 14);
}
// Test that aborted wrap search works returns a result marked as incomplete.
TEST_F(SearchLineWrapsTestFixture, AbortedSearch) {
const std::vector<TokenInfo> tokens = {
{0, "zz"},
{0, "yyy"},
{0, "xxxx"},
};
CreateTokenInfos(tokens);
UnwrappedLine uwline_in(LevelsToSpaces(1), pre_format_tokens_.begin());
AddFormatTokens(&uwline_in);
EXPECT_EQ(uwline_in.Size(), tokens.size());
auto& ftokens_in = pre_format_tokens_;
ftokens_in[0].before.break_penalty = 1;
ftokens_in[0].before.spaces_required = 77; // should be ignored
ftokens_in[1].before.break_penalty = 1;
ftokens_in[1].before.spaces_required = 1;
ftokens_in[2].before.break_penalty = 1;
ftokens_in[2].before.spaces_required = 1;
// Intentionally limit search space to a small count to force early abort.
const auto formatted_lines = verible::SearchLineWraps(uwline_in, style_, 2);
const FormattedExcerpt& formatted_line = formatted_lines.front();
EXPECT_EQ(formatted_line.Tokens().size(), tokens.size());
EXPECT_FALSE(formatted_line.CompletedFormatting());
// The resulting state is unpredictable, because the search terminated early.
// So we don't check any other properties of the formatted_line.
}
} // namespace
} // namespace verible