dtab 0.2.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtab
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Delta Tab: config files made of tab-separated paths. One line is one path into a tree.
5
5
  Project-URL: Homepage, https://github.com/RyannDaGreat/dtab
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: fire
9
9
 
10
- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
10
+ <!-- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p> -->
11
+ <!-- <img width="6912" height="4188" alt="image" src="https://github.com/user-attachments/assets/db10af67-b7ba-4e42-a808-946c8cdcc2c7" /> -->
12
+ <img width="6912" height="2940" alt="image" src="https://github.com/user-attachments/assets/24f2187b-9e71-45e6-9a4b-427ad00e48c8" />
13
+
14
+
15
+
11
16
  <p align="center"><sub>Logo made with <a href="https://github.com/RyannDaGreat/SvelteLib">PowerRP</a>. <a href="https://ryanndagreat.github.io/SvelteLib/?repo=RyannDaGreat/dtab">Open it in PowerRP</a> to edit it (source: <code>assets/doc.json</code>).</sub></p>
12
17
 
13
18
  # dtab
@@ -32,7 +37,7 @@ dtab.parse(open("scene.dtab").read())
32
37
 
33
38
  - Less to look at. No braces, quotes, or commas between values. A file with its tabs aligned reads like pseudocode, and is easy to write by hand, even on paper.
34
39
  - Simple. Six rules, one pass, about 70 lines per implementation.
35
- - Everything is addressable. There are no lists, so every value has a dotted path: `config.deltas.l1.position.x` works with EasyDict in Python and plain property access in JavaScript.
40
+ - Everything is addressable. There are no lists, so every value has a path: `config.deltas.l1.position.x` with EasyDict in Python or plain property access in JavaScript, brackets for keys like `file.json`.
36
41
  - You choose the shape. Lines stack, and `c,d` writes one value under several keys, so the same tree can be written wide, deep, or on one line, trading horizontal space for vertical. These are the same file:
37
42
 
38
43
  ```
@@ -59,8 +64,9 @@ dtab.parse(open("scene.dtab").read())
59
64
  - An entry that starts with a space is a comment.
60
65
  - `$key` starts a multiline string. See below.
61
66
 
62
- Every value is a string. Cast the ones you need. Keys are identifiers (letters, digits, underscores),
63
- so attribute access like `config.deltas.l1` works with EasyDict and friends.
67
+ Every value is a string. Cast the ones you need. A key is any run of letters, digits, `_`, `.`, `-` and `/`,
68
+ so `file.json`, `2026-09-07`, `assets/logo` and `0` are keys. Keys that happen to be identifiers work as attributes
69
+ (`config.deltas.l1` with EasyDict and friends); the rest are reached with brackets (`config["file.json"]`).
64
70
 
65
71
  ## Multiline strings
66
72
 
@@ -92,8 +98,8 @@ the value. Unknown tags are plain text, never an error.
92
98
 
93
99
  - **Python**: `pip install dtab` then `import dtab`
94
100
  - **JavaScript**: `npm install deltatab` then `const dtab = require('deltatab')`, or `<script src="https://cdn.jsdelivr.net/npm/deltatab/dtab.js">` for `window.dtab`
95
- - **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, and gives the file a Δ icon in NERDTree if vim-devicons is installed.
96
- - **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting plus a Δ file icon.
101
+ - **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, gives the file a Δ icon in NERDTree if vim-devicons is installed, `:DtabPreview` opens a split showing the tree as JSON that follows your edits (needs `+python3`), and `J` joins the line below with a tab (deep form to wide form) whenever that keeps the tree the same.
102
+ - **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting, a Δ file icon, and a live JSON preview of the file beside it (the preview button in the title bar, or Cmd+K V).
97
103
 
98
104
  ## API
99
105
 
@@ -1,4 +1,9 @@
1
- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
1
+ <!-- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p> -->
2
+ <!-- <img width="6912" height="4188" alt="image" src="https://github.com/user-attachments/assets/db10af67-b7ba-4e42-a808-946c8cdcc2c7" /> -->
3
+ <img width="6912" height="2940" alt="image" src="https://github.com/user-attachments/assets/24f2187b-9e71-45e6-9a4b-427ad00e48c8" />
4
+
5
+
6
+
2
7
  <p align="center"><sub>Logo made with <a href="https://github.com/RyannDaGreat/SvelteLib">PowerRP</a>. <a href="https://ryanndagreat.github.io/SvelteLib/?repo=RyannDaGreat/dtab">Open it in PowerRP</a> to edit it (source: <code>assets/doc.json</code>).</sub></p>
3
8
 
4
9
  # dtab
@@ -23,7 +28,7 @@ dtab.parse(open("scene.dtab").read())
23
28
 
24
29
  - Less to look at. No braces, quotes, or commas between values. A file with its tabs aligned reads like pseudocode, and is easy to write by hand, even on paper.
25
30
  - Simple. Six rules, one pass, about 70 lines per implementation.
26
- - Everything is addressable. There are no lists, so every value has a dotted path: `config.deltas.l1.position.x` works with EasyDict in Python and plain property access in JavaScript.
31
+ - Everything is addressable. There are no lists, so every value has a path: `config.deltas.l1.position.x` with EasyDict in Python or plain property access in JavaScript, brackets for keys like `file.json`.
27
32
  - You choose the shape. Lines stack, and `c,d` writes one value under several keys, so the same tree can be written wide, deep, or on one line, trading horizontal space for vertical. These are the same file:
28
33
 
29
34
  ```
@@ -50,8 +55,9 @@ dtab.parse(open("scene.dtab").read())
50
55
  - An entry that starts with a space is a comment.
51
56
  - `$key` starts a multiline string. See below.
52
57
 
53
- Every value is a string. Cast the ones you need. Keys are identifiers (letters, digits, underscores),
54
- so attribute access like `config.deltas.l1` works with EasyDict and friends.
58
+ Every value is a string. Cast the ones you need. A key is any run of letters, digits, `_`, `.`, `-` and `/`,
59
+ so `file.json`, `2026-09-07`, `assets/logo` and `0` are keys. Keys that happen to be identifiers work as attributes
60
+ (`config.deltas.l1` with EasyDict and friends); the rest are reached with brackets (`config["file.json"]`).
55
61
 
56
62
  ## Multiline strings
57
63
 
@@ -83,8 +89,8 @@ the value. Unknown tags are plain text, never an error.
83
89
 
84
90
  - **Python**: `pip install dtab` then `import dtab`
85
91
  - **JavaScript**: `npm install deltatab` then `const dtab = require('deltatab')`, or `<script src="https://cdn.jsdelivr.net/npm/deltatab/dtab.js">` for `window.dtab`
86
- - **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, and gives the file a Δ icon in NERDTree if vim-devicons is installed.
87
- - **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting plus a Δ file icon.
92
+ - **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, gives the file a Δ icon in NERDTree if vim-devicons is installed, `:DtabPreview` opens a split showing the tree as JSON that follows your edits (needs `+python3`), and `J` joins the line below with a tab (deep form to wide form) whenever that keeps the tree the same.
93
+ - **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting, a Δ file icon, and a live JSON preview of the file beside it (the preview button in the title bar, or Cmd+K V).
88
94
 
89
95
  ## API
90
96
 
@@ -1,13 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtab
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Delta Tab: config files made of tab-separated paths. One line is one path into a tree.
5
5
  Project-URL: Homepage, https://github.com/RyannDaGreat/dtab
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  Requires-Dist: fire
9
9
 
10
- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
10
+ <!-- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p> -->
11
+ <!-- <img width="6912" height="4188" alt="image" src="https://github.com/user-attachments/assets/db10af67-b7ba-4e42-a808-946c8cdcc2c7" /> -->
12
+ <img width="6912" height="2940" alt="image" src="https://github.com/user-attachments/assets/24f2187b-9e71-45e6-9a4b-427ad00e48c8" />
13
+
14
+
15
+
11
16
  <p align="center"><sub>Logo made with <a href="https://github.com/RyannDaGreat/SvelteLib">PowerRP</a>. <a href="https://ryanndagreat.github.io/SvelteLib/?repo=RyannDaGreat/dtab">Open it in PowerRP</a> to edit it (source: <code>assets/doc.json</code>).</sub></p>
12
17
 
13
18
  # dtab
@@ -32,7 +37,7 @@ dtab.parse(open("scene.dtab").read())
32
37
 
33
38
  - Less to look at. No braces, quotes, or commas between values. A file with its tabs aligned reads like pseudocode, and is easy to write by hand, even on paper.
34
39
  - Simple. Six rules, one pass, about 70 lines per implementation.
35
- - Everything is addressable. There are no lists, so every value has a dotted path: `config.deltas.l1.position.x` works with EasyDict in Python and plain property access in JavaScript.
40
+ - Everything is addressable. There are no lists, so every value has a path: `config.deltas.l1.position.x` with EasyDict in Python or plain property access in JavaScript, brackets for keys like `file.json`.
36
41
  - You choose the shape. Lines stack, and `c,d` writes one value under several keys, so the same tree can be written wide, deep, or on one line, trading horizontal space for vertical. These are the same file:
37
42
 
38
43
  ```
@@ -59,8 +64,9 @@ dtab.parse(open("scene.dtab").read())
59
64
  - An entry that starts with a space is a comment.
60
65
  - `$key` starts a multiline string. See below.
61
66
 
62
- Every value is a string. Cast the ones you need. Keys are identifiers (letters, digits, underscores),
63
- so attribute access like `config.deltas.l1` works with EasyDict and friends.
67
+ Every value is a string. Cast the ones you need. A key is any run of letters, digits, `_`, `.`, `-` and `/`,
68
+ so `file.json`, `2026-09-07`, `assets/logo` and `0` are keys. Keys that happen to be identifiers work as attributes
69
+ (`config.deltas.l1` with EasyDict and friends); the rest are reached with brackets (`config["file.json"]`).
64
70
 
65
71
  ## Multiline strings
66
72
 
@@ -92,8 +98,8 @@ the value. Unknown tags are plain text, never an error.
92
98
 
93
99
  - **Python**: `pip install dtab` then `import dtab`
94
100
  - **JavaScript**: `npm install deltatab` then `const dtab = require('deltatab')`, or `<script src="https://cdn.jsdelivr.net/npm/deltatab/dtab.js">` for `window.dtab`
95
- - **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, and gives the file a Δ icon in NERDTree if vim-devicons is installed.
96
- - **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting plus a Δ file icon.
101
+ - **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, gives the file a Δ icon in NERDTree if vim-devicons is installed, `:DtabPreview` opens a split showing the tree as JSON that follows your edits (needs `+python3`), and `J` joins the line below with a tab (deep form to wide form) whenever that keeps the tree the same.
102
+ - **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting, a Δ file icon, and a live JSON preview of the file beside it (the preview button in the title bar, or Cmd+K V).
97
103
 
98
104
  ## API
99
105
 
@@ -21,7 +21,8 @@ Rules:
21
21
  - `$key` is a multiline leaf: its value is the entries after it on its line plus every line indented
22
22
  under it, one line each, with their common indentation removed. A word after the key on the `$` line
23
23
  (`$query sql`) is a language tag for editors and is not part of the value.
24
- - Keys are identifiers (str.isidentifier), so trees are EasyDict-friendly. Every value is a string.
24
+ - Keys are one or more letters, digits, or KEY_PUNCTUATION (`_.-/`), so `file.json`, `a/b` and `123aa` are keys. Keys that
25
+ are also Python identifiers work as attributes (config.deltas.l1). Every value is a string.
25
26
 
26
27
  Single pass, one stack, O(total characters).
27
28
  """
@@ -29,12 +30,14 @@ Single pass, one stack, O(total characters).
29
30
  import json
30
31
  import re
31
32
 
32
- __version__ = "0.2.0" # SEMANTIC BINDING: dtab-version (also package.json "version")
33
+ __version__ = "0.3.1" # SEMANTIC BINDING: dtab-version (also package.json "version")
33
34
 
34
35
  KEY_SEPARATOR = "," # a,b writes the same value under each key
35
36
  BLOCK_PREFIX = "$" # $key: a leaf whose value is the lines under it ($ as in string)
36
- KEY_RULE = "keys must be identifiers (letters, digits, underscores, not starting with a digit)"
37
+ KEY_PUNCTUATION = "_.-/" # Allowed in keys besides letters and digits. SEMANTIC BINDING: dtab-key-punctuation
38
+ KEY_RULE = "keys may contain only letters, digits and " + " ".join(KEY_PUNCTUATION)
37
39
  _TAB_RUN = re.compile(r"\t+") # Several tabs in a row are one separator, so columns can be aligned
40
+ _KEY = re.compile(r"[\w" + re.escape(KEY_PUNCTUATION) + "]+") # \w: letters, digits, _ (Unicode); the rest is reserved for syntax
38
41
 
39
42
 
40
43
  def parse(text):
@@ -57,9 +60,11 @@ def parse(text):
57
60
  {'query': 'SELECT *\\n\\n\\tFROM users', 'next': '1'}
58
61
  >>> parse('$cmd\\tpip install rp\\tpython train.py')
59
62
  {'cmd': 'pip install rp\\npython train.py'}
60
- >>> parse('a\\tb 1\\nc.d\\te 2')
63
+ >>> parse('a\\tb 1\\nfile.json\\tsize 2\\n123aa 3')
64
+ {'a': {'b': '1'}, 'file.json': {'size': '2'}, '123aa': '3'}
65
+ >>> parse('a\\tb 1\\nc|d\\te 2')
61
66
  Traceback (most recent call last):
62
- ValueError: dtab line 2: invalid key 'c.d': keys must be identifiers (letters, digits, underscores, not starting with a digit)
67
+ ValueError: dtab line 2: invalid key 'c|d': keys may contain only letters, digits and _ . - /
63
68
  """
64
69
  root = {}
65
70
  stack = [(-1, [root])] # (indent, nodes that deeper lines nest into)
@@ -126,19 +131,19 @@ def stringify(tree):
126
131
 
127
132
  def _key_names(key, line_number, allow_commas):
128
133
  """
129
- Pure function (raises ValueError). The identifiers a key stands for: `a,b` is two while parsing,
130
- and a stringify key must be a single bare identifier.
134
+ Pure function (raises ValueError). The names a key stands for: `a,b` is two while parsing, and a
135
+ stringify key must be a single name.
131
136
 
132
137
  Examples:
133
- >>> _key_names('l1,l2', 1, True), _key_names('table_bottom', None, False)
134
- (['l1', 'l2'], ['table_bottom'])
138
+ >>> _key_names('l1,l2', 1, True), _key_names('file-thing.json', None, False)
139
+ (['l1', 'l2'], ['file-thing.json'])
135
140
  >>> _key_names('a,b', None, False)
136
141
  Traceback (most recent call last):
137
- ValueError: dtab: invalid key 'a,b': keys must be identifiers (letters, digits, underscores, not starting with a digit)
142
+ ValueError: dtab: invalid key 'a,b': keys may contain only letters, digits and _ . - /
138
143
  """
139
144
  names = key.split(KEY_SEPARATOR) if allow_commas else [key]
140
145
  for name in names:
141
- if not name.isidentifier():
146
+ if not _KEY.fullmatch(name):
142
147
  where = " line %d" % line_number if line_number else ""
143
148
  raise ValueError("dtab%s: invalid key %r: %s" % (where, key, KEY_RULE))
144
149
  return names
@@ -0,0 +1,278 @@
1
+ """
2
+ Tests for dtab. Run with no arguments from the repo root:
3
+
4
+ python test/test_dtab.py
5
+
6
+ Checks:
7
+ 1. dtab.py doctests.
8
+ 2. dtab.py and dtab.js agree with the ORIGINAL Lab-In-A-Cube djson.js (vendored untouched in
9
+ test/original/, run under node by test/run_original.js with its raw-string leaf option) on every
10
+ sample except deviations.dtab.
11
+ 3. dtab.py and dtab.js agree with each other on every sample; the deviating samples match their goldens
12
+ in test/expected/, which were diffed against the original when written. The differences are exactly:
13
+ blank lines are ignored, an object can overwrite a leaf, comma keys respect line order (in game_config
14
+ that is one leaf, deltas.initial.l1.intensity, where the original ignored the later `l1 intensity 1`),
15
+ and `$` multiline blocks, which the original did not have.
16
+ 4. parse(stringify(parse(text))) == parse(text) for every sample.
17
+ 5. The key rule (letters, digits, _ . -): bad keys are rejected with a line number in parse and in
18
+ stringify, and the Python and JS character classes agree on a set of Unicode probes.
19
+ 6. node test/test_dtab.js.
20
+ 7. Vim: the syntax groups over test/samples/highlight.dtab match test/expected/highlight.txt byte by byte
21
+ (that sample deliberately contains invalid keys, so it is not parsed), embedded languages, the Tab and
22
+ shift keys, J, :DtabPreview, and plugin/dtab.vim sets the filetype when the repo is on 'runtimepath',
23
+ which is what Vundle and vim-plug do.
24
+ 8. docs/index.html in headless Chrome (test/test_web.js), skipped with a message if puppeteer is not
25
+ installed (`npm install --no-save puppeteer && npx puppeteer browsers install chrome`).
26
+ 9. The VS Code extension's TextMate grammar tokenizes the vim highlight sample the same way vim does,
27
+ and its indentation and preview logic (test/test_vscode.js), skipped if vscode-textmate is not installed
28
+ (`npm install --no-save vscode-textmate vscode-oniguruma`).
29
+ 10. The JSON preview inside the installed VS Code, in an isolated profile (test/test_vscode_live.js),
30
+ skipped if @vscode/test-electron is not installed.
31
+
32
+ Needs: python 3, node, vim. The optional packages go in together, since an `npm install --no-save` removes
33
+ the ones it was not given: `npm install --no-save puppeteer vscode-textmate vscode-oniguruma @vscode/test-electron`.
34
+ """
35
+
36
+ import doctest
37
+ import json
38
+ import subprocess
39
+ import sys
40
+ import tempfile
41
+ from pathlib import Path
42
+
43
+ ROOT = Path(__file__).resolve().parent.parent
44
+ HIGHLIGHT_SAMPLE = ROOT / "test" / "samples" / "highlight.dtab"
45
+ SAMPLES = sorted(path for path in (ROOT / "test" / "samples").glob("*.dtab") if path != HIGHLIGHT_SAMPLE)
46
+ DEVIATING = {"deviations.dtab", "game_config.dtab", "multiline.dtab"} # multiline: $ blocks did not exist in the original
47
+ IDENTIFIER_PROBES = ["café", "变量", "x²", "_x", "0x", "a-b", "ok_1", "ª", "Ⅻ", "℘", "ℕ", "𝔸", "a.b", "é1", "1é", "a/b", "a:b", "ä-ö.ü", "١٢٣", "a~b"] # the key rule, py vs js
48
+
49
+ sys.path.insert(0, str(ROOT))
50
+ import dtab # noqa: E402
51
+
52
+
53
+ def run(*command):
54
+ """Query (runs a subprocess). The command's stdout."""
55
+ return subprocess.run(command, capture_output=True, text=True, check=True, cwd=ROOT).stdout
56
+
57
+
58
+ def raises_value_error(function, *fragments):
59
+ """Query (calls function). Asserts function() raises ValueError mentioning every fragment."""
60
+ try:
61
+ function()
62
+ except ValueError as error:
63
+ for fragment in fragments:
64
+ assert fragment in str(error), "error %r does not mention %r" % (str(error), fragment)
65
+ return
66
+ raise AssertionError("expected a ValueError mentioning %r" % (fragments,))
67
+
68
+
69
+ def test_doctests():
70
+ failed, _ = doctest.testmod(dtab)
71
+ assert failed == 0, "dtab.py doctests failed"
72
+
73
+
74
+ def test_readers_agree():
75
+ for path in SAMPLES:
76
+ python_tree = dtab.parse(path.read_text())
77
+ js_tree = json.loads(run("node", "dtab.js", str(path)))
78
+ assert python_tree == js_tree, "%s: dtab.py and dtab.js disagree" % path.name
79
+ if path.name in DEVIATING:
80
+ golden = json.loads((ROOT / "test" / "expected" / (path.stem + ".json")).read_text())
81
+ assert python_tree == golden, "%s: does not match golden" % path.name
82
+ else:
83
+ original = json.loads(run("node", "test/run_original.js", str(path)))
84
+ assert python_tree == original, "%s: does not match the original djson.js" % path.name
85
+
86
+
87
+ def test_round_trips():
88
+ for path in SAMPLES:
89
+ once = dtab.parse(path.read_text())
90
+ assert dtab.parse(dtab.stringify(once)) == once, "%s: did not round trip" % path.name
91
+
92
+
93
+ def test_key_rule():
94
+ for text, fragments in [
95
+ ("a\tc|d 1", ["line 1", "'c|d'"]),
96
+ ("ok 1\n\tk:v 2", ["line 2", "'k:v'"]),
97
+ ("~scope\n\tx 1", ["'~scope'"]),
98
+ ("log\t@ e", ["'@'"]),
99
+ ("a,b#c\tx 1", ["'a,b#c'"]),
100
+ ("\"quoted\" 1", ["'\"quoted\"'"]),
101
+ ]:
102
+ raises_value_error(lambda: dtab.parse(text), *fragments)
103
+ assert dtab.parse("123aa 1\nfile.json 2\nfile-thing.json 3\n123.json-yaml 4\nitems 5\n_p 6\ncafé 7\n-x 8\nassets/logo.png 9\n/ 10") == {
104
+ "123aa": "1", "file.json": "2", "file-thing.json": "3", "123.json-yaml": "4", "items": "5", "_p": "6", "café": "7", "-x": "8", "assets/logo.png": "9", "/": "10"}
105
+ for tree in [{"a b": "1"}, {"a,b": "1"}, {"a|b": "1"}, {"": "1"}]:
106
+ raises_value_error(lambda: dtab.stringify(tree), "dtab")
107
+ assert dtab.parse(dtab.stringify({0: "a", "file.json": "b"})) == {"0": "a", "file.json": "b"} # int keys are written as text
108
+ for value in ["x\ny", "x\ty", "a\n\n\tb\n c", "#!/bin/bash\necho hi", ""]:
109
+ assert dtab.parse(dtab.stringify({"a": value})) == {"a": value}, "multiline round trip failed for %r" % value
110
+ def accepted(probe):
111
+ try:
112
+ dtab.parse(probe + " 1")
113
+ return True
114
+ except ValueError:
115
+ return False
116
+ python_verdicts = [accepted(probe) for probe in IDENTIFIER_PROBES]
117
+ js_verdicts = json.loads(run(
118
+ "node", "-e",
119
+ "const d = require('./dtab.js'); console.log(JSON.stringify(%s.map(p => { try { d.parse(p + ' 1'); return true } catch { return false } })))"
120
+ % json.dumps(IDENTIFIER_PROBES),
121
+ ))
122
+ assert python_verdicts == js_verdicts, "identifier rule differs: %s" % [
123
+ (probe, py, js) for probe, py, js in zip(IDENTIFIER_PROBES, python_verdicts, js_verdicts) if py != js]
124
+
125
+
126
+ def test_js_suite():
127
+ subprocess.run(["node", "test/test_dtab.js"], check=True, cwd=ROOT)
128
+
129
+
130
+ def vim(*commands):
131
+ """Query (runs vim headless). Runs the -c commands in order in a clean vim and returns nothing."""
132
+ arguments = ["vim", "-N", "-u", "NONE", "-i", "NONE", "-n", "-es", "-c", "set shortmess+=A"] # -n, +=A: no swap files, no ATTENTION prompt
133
+ for command in commands:
134
+ arguments += ["-c", command]
135
+ subprocess.run(arguments + ["-c", "qa!"], check=True, cwd=ROOT)
136
+
137
+
138
+ def test_vim_highlighting():
139
+ with tempfile.TemporaryDirectory() as directory:
140
+ out = Path(directory) / "highlights.txt"
141
+ vim("syntax on", "source dtab.vim", "edit " + str(HIGHLIGHT_SAMPLE),
142
+ "source test/dump_highlights.vim", "call DumpHighlights('%s')" % out)
143
+ got = out.read_text()
144
+ expected = (ROOT / "test" / "expected" / "highlight.txt").read_text()
145
+ assert got == expected, "vim highlighting differs:\nexpected:\n%s\ngot:\n%s" % (expected, got)
146
+
147
+
148
+ def test_vim_embedded_languages():
149
+ """A tagged $ block (at line start or after other entries) and a shebang block get the language's own groups."""
150
+ with tempfile.TemporaryDirectory() as directory:
151
+ sample = Path(directory) / "embedded.dtab"
152
+ sample.write_text("$query sql\n\tSELECT name FROM t\nconfig\tdb\t$init sql\n\t\tCREATE TABLE t\n$s\n\t#!/bin/bash\n\techo hi\nafter 1\n")
153
+ out = Path(directory) / "groups.txt"
154
+ vim("syntax on", "source dtab.vim", "edit " + str(sample),
155
+ "call writefile([synIDattr(synID(2,2,1),'name'), synIDattr(synID(3,12,1),'name'), synIDattr(synID(4,3,1),'name'), synIDattr(synID(7,2,1),'name'), synIDattr(synID(8,1,1),'name')], '%s')" % out)
156
+ groups = out.read_text().split()
157
+ assert groups == ["sqlStatement", "dtabBlockKey", "sqlStatement", "shStatement", "dtabLeafKey"], groups
158
+
159
+
160
+ def test_vim_tab_key():
161
+ """In insert mode, Tab inside a $ block (past the line's tabs) inserts spaces; elsewhere a tab."""
162
+ with tempfile.TemporaryDirectory() as directory:
163
+ sample = Path(directory) / "tab.dtab"
164
+ sample.write_text("$code python\n\tdef f():\n\t\nafter 1\nx\n\t$code\n\t\tbody\n\t\n\n")
165
+ out = Path(directory) / "lines.txt"
166
+ vim("syntax on", "source dtab.vim", "edit " + str(sample),
167
+ "call cursor(3, 2) | execute \"normal a\\<Tab>return 1\" | call cursor(3, 1) | execute \"normal i\\<Tab>\" | call cursor(4, 6) | execute \"normal i\\<Tab>\"",
168
+ "call cursor(8, 1) | execute \"normal A\\<Tab>\" | call cursor(9, 1) | execute \"normal A\\<Tab>\\<Esc>A\\<Tab>\\<Esc>A\\<Tab>\"",
169
+ "call writefile([getline(3), getline(4), getline(8), getline(9)], '%s')" % out)
170
+ lines = out.read_text().split("\n")
171
+ assert lines[0] == "\t\t return 1", repr(lines[0]) # spaces past the block's tab; a tab at the line start
172
+ assert lines[1] == "after\t 1", repr(lines[1]) # a tab outside the block
173
+ assert lines[2] == "\t\t", repr(lines[2]) # a whitespace line at the $ line's depth is structure: a tab
174
+ assert lines[3] == "\t\t ", repr(lines[3]) # an empty line: tabs until the line is deeper than the $ line, then spaces
175
+
176
+
177
+ def test_vim_shift_keys():
178
+ """>> and << (and visual > <) shift block lines by four spaces, other lines by a tab."""
179
+ with tempfile.TemporaryDirectory() as directory:
180
+ sample = Path(directory) / "shift.dtab"
181
+ sample.write_text("before 1\n$code python\n\tdef f():\n\t return 1\n")
182
+ out = Path(directory) / "lines.txt"
183
+ vim("syntax on", "source dtab.vim", "edit " + str(sample),
184
+ "execute '1normal >>' | execute '1normal <<' | execute '3normal >>' | execute '4normal <<' | execute '2normal Vjj>'",
185
+ "execute '3normal 2>j' | execute '3normal <j' | execute '3normal >2j'",
186
+ "call writefile(getline(1, '$'), '%s')" % out)
187
+ lines = out.read_text().split("\n")
188
+ assert lines[0] == "before 1", repr(lines[0]) # >> gave it a tab, << took it away
189
+ assert lines[1] == "\t$code python", repr(lines[1]) # visual > over the $ line and its block: tabs for all
190
+ assert lines[2] == "\t\t def f():", repr(lines[2]) # >> inside the block: spaces; the block's tab; then 2>j, <j, >2j: net one more level
191
+ assert lines[3] == "\t\t return 1", repr(lines[3]) # << removed the spaces; 2>j <j >2j: net one level
192
+
193
+
194
+ def test_vim_join():
195
+ """J joins structure lines with a tab when the tree stays the same, refuses when a line would change parent, joins block text like vim, drops blanks, takes counts and visual ranges."""
196
+ text = ("deltas\n\tl1\n\t\tposition\n\t\t\tx 1\n\t\t\ty .5\nafter 1\nl1\nl2\n$code python\n\tdef f():\n\t return 1\nz 1\n\nend 1\n"
197
+ "u 1\n\tobj\n\tsib 2\nlast 1\nt 1\t\n\tswallowed 2\nk\tleaf 1\n note\nv 1\n note\n\tw 2\n")
198
+ with tempfile.TemporaryDirectory() as directory:
199
+ sample = Path(directory) / "join.dtab"
200
+ sample.write_text(text)
201
+ out = Path(directory) / "lines.txt"
202
+ vim("syntax on", "source dtab.vim", "edit " + str(sample),
203
+ # 3J: deep to wide; J J: the two leaves; J: refused (after 1 is a sibling of deltas); after 1 + l1: allowed, the upper
204
+ # steps into nothing; + l2: refused, l1 steps in; block text joins like vim's J; a $ line keeps its block below it: refused
205
+ "execute '1normal 3J' | execute '1normal J' | execute '1normal J' | execute '1normal J' | execute '2normal J' | execute '2normal J' | execute '5normal J' | execute '4normal J'",
206
+ # a blank line is dropped; visual J; u 1 + obj: refused, obj would capture sib; obj + sib: refused, obj steps in;
207
+ # t 1<Tab> + deeper: refused, the empty key would vanish; k<Tab>leaf 1 + a comment: allowed; v 1 + a comment that a
208
+ # deeper line follows: allowed, since v 1 steps into nothing and w 2 keeps its parent
209
+ "execute '6normal J' | execute '6normal VjJ' | execute '7normal J' | execute '8normal J' | execute '11normal J' | execute '13normal J' | execute '14normal J'",
210
+ "call writefile(getline(1, '$'), '%s')" % out)
211
+ lines = out.read_text().split("\n")[:-1]
212
+ assert lines == ["deltas\tl1\tposition\tx 1\ty .5", "after 1\tl1", "l2", "$code python", "\tdef f(): return 1", "z 1\tend 1",
213
+ "u 1", "\tobj", "\tsib 2", "last 1", "t 1\t", "\tswallowed 2", "k\tleaf 1\t note", "v 1\t note", "\tw 2"], lines
214
+ joined = dtab.parse("\n".join(lines))
215
+ expected = dtab.parse(text)
216
+ expected["code"] = "def f(): return 1" # the one join that changes a value: two lines of block text, joined like vim does
217
+ assert joined == expected, joined
218
+
219
+
220
+ def test_vim_preview():
221
+ """:DtabPreview opens a JSON split of the buffer's tree that follows edits (showing the parser's message when the text is broken), and closes it when repeated."""
222
+ sample = SAMPLES[0]
223
+ with tempfile.TemporaryDirectory() as directory:
224
+ out = Path(directory) / "preview.txt"
225
+ vim("syntax on", "source dtab.vim", "edit " + str(sample),
226
+ "DtabPreview | call writefile(getbufline(b:dtab_preview, 1, '$') + ['---'], '%s')" % out,
227
+ "call setline(1, 'bad|key 1') | doautocmd TextChanged",
228
+ "call writefile(getbufline(b:dtab_preview, 1, '$') + ['---', getbufvar(b:dtab_preview, '&filetype')], '%s', 'a')" % out,
229
+ "DtabPreview | call writefile([bufwinnr(b:dtab_preview)], '%s', 'a')" % out)
230
+ tree, message, closed = out.read_text().split("---\n")
231
+ assert json.loads(tree) == dtab.parse(sample.read_text()), "the preview is not the parsed tree"
232
+ assert message.startswith("dtab line 1: invalid key 'bad|key'"), "the preview did not follow the edit: %r" % message
233
+ assert closed == "json\n-1\n", "expected a json split that the second :DtabPreview closes: %r" % closed
234
+
235
+
236
+ def test_vim_plugin_shim():
237
+ with tempfile.TemporaryDirectory() as directory:
238
+ out = Path(directory) / "filetype.txt"
239
+ vim("set rtp+=" + str(ROOT), "runtime! plugin/*.vim", "edit " + str(SAMPLES[0]),
240
+ "call writefile([&filetype], '%s')" % out)
241
+ assert out.read_text().strip() == "dtab", "plugin/dtab.vim did not set the filetype"
242
+
243
+
244
+ def test_web_demo():
245
+ if not (ROOT / "node_modules" / "puppeteer").is_dir():
246
+ print(" (skipped: puppeteer not installed)")
247
+ return
248
+ subprocess.run(["node", "test/test_web.js"], check=True, cwd=ROOT)
249
+
250
+
251
+ def test_vscode_grammar():
252
+ # The committed grammar and language configuration are what vscode/make_grammar.py generates.
253
+ with tempfile.TemporaryDirectory() as directory:
254
+ sys.path.insert(0, str(ROOT / "vscode"))
255
+ import make_grammar # noqa: E402
256
+ for written in make_grammar.write(directory):
257
+ committed = ROOT / "vscode" / Path(written).relative_to(directory)
258
+ assert Path(written).read_text() == committed.read_text(), "%s is stale: run python vscode/make_grammar.py" % committed
259
+ if not (ROOT / "node_modules" / "vscode-textmate").is_dir():
260
+ print(" (skipped: vscode-textmate not installed)")
261
+ return
262
+ subprocess.run(["node", "test/test_vscode.js"], check=True, cwd=ROOT)
263
+
264
+
265
+ def test_vscode_live():
266
+ """The JSON preview inside the installed VS Code (test/test_vscode_live.js), skipped without @vscode/test-electron or the app."""
267
+ if not (ROOT / "node_modules" / "@vscode" / "test-electron").is_dir() or not Path("/Applications/Visual Studio Code.app").is_dir():
268
+ print(" (skipped: @vscode/test-electron or VS Code not installed)")
269
+ return
270
+ subprocess.run(["node", "test/test_vscode_live.js"], check=True, cwd=ROOT)
271
+
272
+
273
+ if __name__ == "__main__":
274
+ for test in [test_doctests, test_readers_agree, test_round_trips, test_key_rule, test_js_suite,
275
+ test_vim_highlighting, test_vim_embedded_languages, test_vim_tab_key, test_vim_shift_keys, test_vim_join, test_vim_preview, test_vim_plugin_shim, test_web_demo, test_vscode_grammar, test_vscode_live]:
276
+ test()
277
+ print("ok " + test.__name__)
278
+ print("All dtab tests passed")
@@ -1,189 +0,0 @@
1
- """
2
- Tests for dtab. Run with no arguments from the repo root:
3
-
4
- python test/test_dtab.py
5
-
6
- Checks:
7
- 1. dtab.py doctests.
8
- 2. dtab.py and dtab.js agree with the ORIGINAL Lab-In-A-Cube djson.js (vendored untouched in
9
- test/original/, run under node by test/run_original.js with its raw-string leaf option) on every
10
- sample except deviations.dtab.
11
- 3. dtab.py and dtab.js agree with each other on every sample; the deviating samples match their goldens
12
- in test/expected/, which were diffed against the original when written. The differences are exactly:
13
- blank lines are ignored, an object can overwrite a leaf, comma keys respect line order (in game_config
14
- that is one leaf, deltas.initial.l1.intensity, where the original ignored the later `l1 intensity 1`),
15
- and `$` multiline blocks, which the original did not have.
16
- 4. parse(stringify(parse(text))) == parse(text) for every sample.
17
- 5. The key rule: bad keys are rejected with a line number in parse and in stringify, and Python's
18
- str.isidentifier and the JS Unicode regex agree on a set of probes.
19
- 6. node test/test_dtab.js.
20
- 7. Vim: the syntax groups over test/samples/highlight.dtab match test/expected/highlight.txt byte by byte
21
- (that sample deliberately contains invalid keys, so it is not parsed), and plugin/dtab.vim sets the
22
- filetype when the repo is on 'runtimepath', which is what Vundle and vim-plug do.
23
- 8. docs/index.html in headless Chrome (test/test_web.js), skipped with a message if puppeteer is not
24
- installed (`npm install --no-save puppeteer && npx puppeteer browsers install chrome`).
25
- 9. The VS Code extension's TextMate grammar tokenizes the vim highlight sample the same way vim does
26
- (test/test_vscode.js), skipped if vscode-textmate is not installed
27
- (`npm install --no-save vscode-textmate vscode-oniguruma`).
28
-
29
- Needs: python 3, node, vim.
30
- """
31
-
32
- import doctest
33
- import json
34
- import subprocess
35
- import sys
36
- import tempfile
37
- from pathlib import Path
38
-
39
- ROOT = Path(__file__).resolve().parent.parent
40
- HIGHLIGHT_SAMPLE = ROOT / "test" / "samples" / "highlight.dtab"
41
- SAMPLES = sorted(path for path in (ROOT / "test" / "samples").glob("*.dtab") if path != HIGHLIGHT_SAMPLE)
42
- DEVIATING = {"deviations.dtab", "game_config.dtab", "multiline.dtab"} # multiline: $ blocks did not exist in the original
43
- IDENTIFIER_PROBES = ["café", "变量", "x²", "_x", "0x", "from", "items", "a-b", "ok_1", "ª", "Ⅻ", "℘", "ℕ", "𝔸", "a.b", "é1", "1é"]
44
-
45
- sys.path.insert(0, str(ROOT))
46
- import dtab # noqa: E402
47
-
48
-
49
- def run(*command):
50
- """Query (runs a subprocess). The command's stdout."""
51
- return subprocess.run(command, capture_output=True, text=True, check=True, cwd=ROOT).stdout
52
-
53
-
54
- def raises_value_error(function, *fragments):
55
- """Query (calls function). Asserts function() raises ValueError mentioning every fragment."""
56
- try:
57
- function()
58
- except ValueError as error:
59
- for fragment in fragments:
60
- assert fragment in str(error), "error %r does not mention %r" % (str(error), fragment)
61
- return
62
- raise AssertionError("expected a ValueError mentioning %r" % (fragments,))
63
-
64
-
65
- def test_doctests():
66
- failed, _ = doctest.testmod(dtab)
67
- assert failed == 0, "dtab.py doctests failed"
68
-
69
-
70
- def test_readers_agree():
71
- for path in SAMPLES:
72
- python_tree = dtab.parse(path.read_text())
73
- js_tree = json.loads(run("node", "dtab.js", str(path)))
74
- assert python_tree == js_tree, "%s: dtab.py and dtab.js disagree" % path.name
75
- if path.name in DEVIATING:
76
- golden = json.loads((ROOT / "test" / "expected" / (path.stem + ".json")).read_text())
77
- assert python_tree == golden, "%s: does not match golden" % path.name
78
- else:
79
- original = json.loads(run("node", "test/run_original.js", str(path)))
80
- assert python_tree == original, "%s: does not match the original djson.js" % path.name
81
-
82
-
83
- def test_round_trips():
84
- for path in SAMPLES:
85
- once = dtab.parse(path.read_text())
86
- assert dtab.parse(dtab.stringify(once)) == once, "%s: did not round trip" % path.name
87
-
88
-
89
- def test_key_rule():
90
- for text, fragments in [
91
- ("a\tcheckpoint.initial 1", ["line 1", "'checkpoint.initial'"]),
92
- ("ok 1\n\t2nd 2", ["line 2", "'2nd'"]),
93
- ("a-b 1", ["'a-b'"]),
94
- ("~scope\n\tx 1", ["'~scope'"]),
95
- ("log\t@ e", ["'@'"]),
96
- ("a,b.c\tx 1", ["'a,b.c'"]),
97
- ]:
98
- raises_value_error(lambda: dtab.parse(text), *fragments)
99
- assert dtab.parse("items 1\nfrom 2\n_private 3\ncafé 4") == {"items": "1", "from": "2", "_private": "3", "café": "4"}
100
- for tree in [{"a b": "1"}, {"a,b": "1"}, {0: "1"}, {"": "1"}]:
101
- raises_value_error(lambda: dtab.stringify(tree), "dtab")
102
- for value in ["x\ny", "x\ty", "a\n\n\tb\n c", "#!/bin/bash\necho hi", ""]:
103
- assert dtab.parse(dtab.stringify({"a": value})) == {"a": value}, "multiline round trip failed for %r" % value
104
- python_verdicts = [probe.isidentifier() for probe in IDENTIFIER_PROBES]
105
- js_verdicts = json.loads(run(
106
- "node", "-e",
107
- "const d = require('./dtab.js'); console.log(JSON.stringify(%s.map(p => { try { d.parse(p + ' 1'); return true } catch { return false } })))"
108
- % json.dumps(IDENTIFIER_PROBES),
109
- ))
110
- assert python_verdicts == js_verdicts, "identifier rule differs: %s" % [
111
- (probe, py, js) for probe, py, js in zip(IDENTIFIER_PROBES, python_verdicts, js_verdicts) if py != js]
112
-
113
-
114
- def test_js_suite():
115
- subprocess.run(["node", "test/test_dtab.js"], check=True, cwd=ROOT)
116
-
117
-
118
- def vim(*commands):
119
- """Query (runs vim headless). Runs the -c commands in order in a clean vim and returns nothing."""
120
- arguments = ["vim", "-N", "-u", "NONE", "-i", "NONE", "-n", "-es", "-c", "set shortmess+=A"] # -n, +=A: no swap files, no ATTENTION prompt
121
- for command in commands:
122
- arguments += ["-c", command]
123
- subprocess.run(arguments + ["-c", "qa!"], check=True, cwd=ROOT)
124
-
125
-
126
- def test_vim_highlighting():
127
- with tempfile.TemporaryDirectory() as directory:
128
- out = Path(directory) / "highlights.txt"
129
- vim("syntax on", "source dtab.vim", "edit " + str(HIGHLIGHT_SAMPLE),
130
- "source test/dump_highlights.vim", "call DumpHighlights('%s')" % out)
131
- got = out.read_text()
132
- expected = (ROOT / "test" / "expected" / "highlight.txt").read_text()
133
- assert got == expected, "vim highlighting differs:\nexpected:\n%s\ngot:\n%s" % (expected, got)
134
-
135
-
136
- def test_vim_embedded_languages():
137
- """A tagged $ block (at line start or after other entries) and a shebang block get the language's own groups."""
138
- with tempfile.TemporaryDirectory() as directory:
139
- sample = Path(directory) / "embedded.dtab"
140
- sample.write_text("$query sql\n\tSELECT name FROM t\nconfig\tdb\t$init sql\n\t\tCREATE TABLE t\n$s\n\t#!/bin/bash\n\techo hi\nafter 1\n")
141
- out = Path(directory) / "groups.txt"
142
- vim("syntax on", "source dtab.vim", "edit " + str(sample),
143
- "call writefile([synIDattr(synID(2,2,1),'name'), synIDattr(synID(3,12,1),'name'), synIDattr(synID(4,3,1),'name'), synIDattr(synID(7,2,1),'name'), synIDattr(synID(8,1,1),'name')], '%s')" % out)
144
- groups = out.read_text().split()
145
- assert groups == ["sqlStatement", "dtabBlockKey", "sqlStatement", "shStatement", "dtabLeafKey"], groups
146
-
147
-
148
- def test_vim_tab_key():
149
- """In insert mode, Tab inside a $ block (past the line's tabs) inserts spaces; elsewhere a tab."""
150
- with tempfile.TemporaryDirectory() as directory:
151
- sample = Path(directory) / "tab.dtab"
152
- sample.write_text("$code python\n\tdef f():\n\t\nafter 1\n")
153
- out = Path(directory) / "lines.txt"
154
- vim("syntax on", "source dtab.vim", "edit " + str(sample),
155
- "call cursor(3, 2) | execute \"normal a\\<Tab>return 1\" | call cursor(3, 1) | execute \"normal i\\<Tab>\" | call cursor(4, 6) | execute \"normal i\\<Tab>\"",
156
- "call writefile([getline(3), getline(4)], '%s')" % out)
157
- lines = out.read_text().split("\n")
158
- assert lines[0] == "\t\t return 1", repr(lines[0]) # spaces past the block's tab; a tab at the line start
159
- assert lines[1] == "after\t 1", repr(lines[1]) # a tab outside the block
160
-
161
-
162
- def test_vim_plugin_shim():
163
- with tempfile.TemporaryDirectory() as directory:
164
- out = Path(directory) / "filetype.txt"
165
- vim("set rtp+=" + str(ROOT), "runtime! plugin/*.vim", "edit " + str(SAMPLES[0]),
166
- "call writefile([&filetype], '%s')" % out)
167
- assert out.read_text().strip() == "dtab", "plugin/dtab.vim did not set the filetype"
168
-
169
-
170
- def test_web_demo():
171
- if not (ROOT / "node_modules" / "puppeteer").is_dir():
172
- print(" (skipped: puppeteer not installed)")
173
- return
174
- subprocess.run(["node", "test/test_web.js"], check=True, cwd=ROOT)
175
-
176
-
177
- def test_vscode_grammar():
178
- if not (ROOT / "node_modules" / "vscode-textmate").is_dir():
179
- print(" (skipped: vscode-textmate not installed)")
180
- return
181
- subprocess.run(["node", "test/test_vscode.js"], check=True, cwd=ROOT)
182
-
183
-
184
- if __name__ == "__main__":
185
- for test in [test_doctests, test_readers_agree, test_round_trips, test_key_rule, test_js_suite,
186
- test_vim_highlighting, test_vim_embedded_languages, test_vim_tab_key, test_vim_plugin_shim, test_web_demo, test_vscode_grammar]:
187
- test()
188
- print("ok " + test.__name__)
189
- print("All dtab tests passed")
File without changes
File without changes
File without changes
File without changes
File without changes