1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#pragma once
///@file

#include "lix/libutil/error.hh"
#include <compare>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <variant>
#include <vector>

namespace nix {
namespace cli_literate_parser {

// ------------------------- NODES -------------------------
//
// To update golden test files while preserving commentary output and other `@`
// directives, we need to keep commentary output around after parsing.

struct BaseNode {
  virtual ~BaseNode() = default;

  virtual auto shouldCompare() const -> bool { return false; }

  virtual auto kind() const -> std::string = 0;
  virtual auto emitNewlineAfter() const -> bool = 0;

  auto operator<=>(const BaseNode & rhs) const = default;
};

/**
 * A node containing text. The text should be identical to how the node was
 * written in the input file.
 */
struct TextNode : BaseNode {
  std::string text;

  explicit TextNode(std::string text) : text(text) {}
  auto operator<=>(const TextNode & rhs) const = default;
};

std::ostream &operator<<(std::ostream &output, const TextNode &node);

// clang-format off
#define DECLARE_TEXT_NODE(NAME, NEEDS_NEWLINE, SHOULD_COMPARE)                 \
  struct NAME : TextNode {                                                     \
    using TextNode::TextNode;                                                  \
    ~NAME() override = default;                                                \
                                                                               \
    auto kind() const -> std::string override { return #NAME; }                \
    auto emitNewlineAfter() const -> bool override { return NEEDS_NEWLINE; }   \
    auto shouldCompare() const -> bool override { return SHOULD_COMPARE; }     \
    auto operator<=>(const NAME & rhs) const = default;                         \
  };
// clang-format on

/* name, needsNewline, shouldCompare */
DECLARE_TEXT_NODE(Prompt, false, false)
DECLARE_TEXT_NODE(Command, true, true)
DECLARE_TEXT_NODE(Output, true, true)
DECLARE_TEXT_NODE(Commentary, true, false)
DECLARE_TEXT_NODE(Args, true, false)
DECLARE_TEXT_NODE(Indent, false, false)

#undef DECLARE_TEXT_NODE

struct ShouldStart : BaseNode {
  bool shouldStart;

  ShouldStart(bool shouldStart) : shouldStart(shouldStart) {}
  ~ShouldStart() override = default;
  auto emitNewlineAfter() const -> bool override { return true; }
  auto kind() const -> std::string override { return "should-start"; }

  auto operator<=>(const ShouldStart &rhs) const = default;
};
std::ostream &operator<<(std::ostream &output, const ShouldStart &node);

/**
 * Any syntax node, including those that are cosmetic.
 */
using Node = std::variant<Prompt, Command, Output, Commentary, Args,
                          ShouldStart, Indent>;

/** Unparses a node into the exact text that would have created it, including a
 * newline at the end if present, if withNewline is set */
void unparseNode(std::ostream &output, const Node &node,
                 bool withNewline = true);

std::string debugNode(const Node &node);
void debugPrint(std::ostream &output, std::vector<Node> &nodes);

/**
 * Override gtest printing for lists of nodes.
 */
void PrintTo(std::vector<Node> const &nodes, std::ostream *output);

/**
 * The result of parsing a test file.
 */
struct ParseResult {
  /**
   * A set of nodes that can be used to reproduce the input file. This is used
   * to implement updating the test files.
   */
  std::vector<Node> syntax;

  /**
   * Extra CLI arguments.
   */
  std::vector<std::string> args;

  /**
   * Should the program start successfully?
   */
  bool shouldStart = false;

  /**
   * Replace `$PWD` with the given value in `args`.
   */
  void interpolatePwd(std::string_view pwd);

  /**
   * Tidy `syntax` to remove unnecessary nodes.
   */
  auto tidyOutputForComparison() -> std::vector<Node>;

  auto debugPrint(std::ostream &output) -> void;
};

/**
 * A parse error.
 */
struct ParseError : BaseException {
  std::string expected;
  std::string rest;

  ParseError(std::string expected, std::string rest)
      : expected(expected), rest(rest) {}

  const char *what() const noexcept override;

private:
  /**
   * Cached formatted contents of `what()`.
   */
  mutable std::optional<std::string> what_;
};

struct Config {
  /**
   * The prompt string to look for.
   */
  std::string prompt;
  /**
   * The number of spaces of indent for commands and output.
   */
  size_t indent = 2;
};

/*
 * A recursive descent parser for literate test cases for CLIs.
 *
 * FIXME: implement merging of these, so you can auto update cases that have
 * comments.
 *
 * Syntax:
 * ```
 * ( COMMENTARY
 * | INDENT PROMPT COMMAND
 * | INDENT OUTPUT
 * | @args ARGS
 * | @should-start ( true | false )) *
 * ```
 *
 * e.g.
 * ```
 * commentary commentary commentary
 * @args --foo
 * @should-start false
 *   nix-repl> :t 1
 *   an integer
 * ```
 *
 * Yields something like:
 * ```
 * Commentary "commentary commentary commentary"
 * Args "--foo"
 * ShouldStart false
 * Command ":t 1"
 * Output "an integer"
 * ```
 *
 * Note: one Output line is generated for each line of the sources, because
 * this is effectively necessary to be able to align them in the future to
 * auto-update tests.
 */
auto parse(std::string input, Config config) -> ParseResult;

}; // namespace cli_literate_parser
}; // namespace nix