| 1 | // vtest retry: 2 |
| 2 | // vtest build: !windows |
| 3 | module main |
| 4 | |
| 5 | import os |
| 6 | import arrays |
| 7 | import v.ast |
| 8 | import document as doc |
| 9 | import markdown |
| 10 | |
| 11 | const vexe_path = @VEXE |
| 12 | const vexe_ = os.quoted_path(vexe_path) |
| 13 | const tpath = os.join_path(os.vtmp_dir(), 'vod_test_module') |
| 14 | |
| 15 | fn testsuite_begin() { |
| 16 | os.rmdir_all(tpath) or {} |
| 17 | os.mkdir_all(tpath)! |
| 18 | os.chdir(tpath)! |
| 19 | } |
| 20 | |
| 21 | fn testsuite_end() { |
| 22 | os.rmdir_all(tpath) or {} |
| 23 | } |
| 24 | |
| 25 | fn test_trim_doc_node_description() { |
| 26 | mod := 'foo' |
| 27 | mut readme := '## Description |
| 28 | |
| 29 | `foo` is a module that provides tools and utility functions to assist in working with bar. |
| 30 | It also assists with composing and testing baz.' |
| 31 | expected := 'is a module that provides tools and utility functions to assist in working with' |
| 32 | res := trim_doc_node_description(mod, readme).trim_space() |
| 33 | assert res == expected |
| 34 | |
| 35 | readme = '# Foo |
| 36 | `foo` is a module that provides tools and utility functions to assist in working with bar. |
| 37 | It also assists with composing and testing baz.' |
| 38 | res2 := trim_doc_node_description(mod, readme).trim_space() |
| 39 | assert res2 == res |
| 40 | } |
| 41 | |
| 42 | fn test_ignore_rules() { |
| 43 | os.write_file('.vdocignore', ['pattern1', 'pattern2', '/path1'].join_lines())! |
| 44 | os.mkdir('subdir')! |
| 45 | os.write_file(os.join_path('subdir', '.vdocignore'), ['pattern3', '/path2'].join_lines())! |
| 46 | rules := IgnoreRules.get('.') |
| 47 | assert rules.patterns['.'] == ['pattern1', 'pattern2'] |
| 48 | assert rules.patterns['./subdir'] == ['pattern3'] |
| 49 | assert rules.paths == { |
| 50 | './path1': true |
| 51 | './subdir/path2': true |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | fn test_get_module_list() { |
| 56 | // For information on leading slash rules, refer to the comments in `IgnoreRules.get`. |
| 57 | ignore_rules := ['bravo', '/echo', '/foxtrot/golf', 'hotel.v/', 'india/juliett'] |
| 58 | os.write_file('.vdocignore', ignore_rules.join_lines())! |
| 59 | |
| 60 | /* Create some submodules. |
| 61 | Modules inside `testdata` and `tests` directories and modules that |
| 62 | only contain `_test.v` files should be ignored by default. */ |
| 63 | // Modules NOT to ignore. |
| 64 | submodules_no_ignore := [ |
| 65 | 'alpha', |
| 66 | 'alpha_bravo', // test `bravo` |
| 67 | 'bravo_charly', // test `bravo` |
| 68 | 'charly', |
| 69 | 'charly/alpha', |
| 70 | 'charly/delta', // test `delta` in separate ignore file in `alpha` |
| 71 | 'charly/echo', // test `/echo` |
| 72 | 'charly/foxtrot/golf', // test `/foxtrot/golf` |
| 73 | 'foxtrot', |
| 74 | 'golf', |
| 75 | 'hotel', // will include a `hotel.v` file, whose pattern is in the ignore list with a trailing slash |
| 76 | ] |
| 77 | // Modules TO ignore. |
| 78 | submodules_to_ignore := [ |
| 79 | 'alpha/bravo', // test `bravo` |
| 80 | 'alpha/delta', // test `delta` in separate ignore file |
| 81 | 'alpha/india/juliett/kilo', // test `india/juliett` |
| 82 | 'bravo', // test `bravo` |
| 83 | 'echo', // test `/echo` |
| 84 | 'foxtrot/golf', // test `/foxtrot/golf` |
| 85 | 'hotel.v', // test `hotel.v/` |
| 86 | 'tests', // test default |
| 87 | 'testdata', // test default |
| 88 | 'testdata/foxtrot', // test default |
| 89 | ] |
| 90 | for p in arrays.append(submodules_no_ignore, submodules_to_ignore) { |
| 91 | os.mkdir_all(p)! |
| 92 | mod_name := p.all_after_last('/') |
| 93 | os.write_file(os.join_path(p, '${mod_name}.v'), 'module ${mod_name}')! |
| 94 | } |
| 95 | // Create a module that only contains a `_test.v` file. |
| 96 | os.mkdir('delta')! |
| 97 | os.write_file(os.join_path('delta', 'delta_test.v'), 'module delta')! |
| 98 | // Add a `.vdocignore` file to a submodule. |
| 99 | os.write_file(os.join_path('alpha', '.vdocignore'), 'delta\n')! |
| 100 | |
| 101 | mod_list := get_modules(tpath) |
| 102 | // dump(mod_list) |
| 103 | assert mod_list.len == submodules_no_ignore.len |
| 104 | for m in submodules_no_ignore.map(os.join_path(tpath, it)) { |
| 105 | assert m in mod_list |
| 106 | } |
| 107 | for m in submodules_to_ignore.map(os.join_path(tpath, it)) { |
| 108 | assert m !in mod_list |
| 109 | } |
| 110 | // `delta` only contains a `_test.v` file. |
| 111 | assert !mod_list.any(it.contains(os.join_path(tpath, 'delta'))) |
| 112 | } |
| 113 | |
| 114 | fn test_html_highlight_escapes_html_tokens() { |
| 115 | table := ast.new_table() |
| 116 | code := 'fn main() { |
| 117 | // <h1>owned</h1> |
| 118 | assert 1 < 2 |
| 119 | }' |
| 120 | highlighted := html_highlight(code, table) |
| 121 | assert highlighted.contains('// <h1>owned</h1>') |
| 122 | assert !highlighted.contains('<h1>owned</h1>') |
| 123 | assert highlighted.contains('<span class="token operator"><</span>') |
| 124 | } |
| 125 | |
| 126 | fn test_get_readme_md_src() { |
| 127 | // a special testcase for `src` dir get_readme |
| 128 | // https://github.com/vlang/v/issues/24232 |
| 129 | |
| 130 | os.mkdir('src')! |
| 131 | os.write_file('v.mod', "Module { |
| 132 | name: 'foobar' |
| 133 | description: 'foobar' |
| 134 | version: '0.0.0' |
| 135 | license: 'MIT' |
| 136 | dependencies: [] |
| 137 | } |
| 138 | ")! |
| 139 | os.write_file('src/foobar.v', 'module foobar |
| 140 | |
| 141 | // square calculates the second power of `x` |
| 142 | pub fn square(x int) int { |
| 143 | return x * x |
| 144 | } |
| 145 | ')! |
| 146 | res := os.execute_opt('${vexe_} doc -m src/ -v') or { panic(err) } |
| 147 | assert res.exit_code == 0 |
| 148 | assert res.output.contains('square') |
| 149 | } |
| 150 | |
| 151 | fn test_gen_modules_toc_skips_hash_links_for_prefix_only_groups() { |
| 152 | mut vd := VDoc{ |
| 153 | cfg: Config{ |
| 154 | is_multi: true |
| 155 | } |
| 156 | } |
| 157 | vd.docs = [ |
| 158 | doc.Doc{ |
| 159 | head: doc.DocNode{ |
| 160 | name: 'main' |
| 161 | } |
| 162 | }, |
| 163 | doc.Doc{ |
| 164 | head: doc.DocNode{ |
| 165 | name: 'db.mysql' |
| 166 | } |
| 167 | }, |
| 168 | doc.Doc{ |
| 169 | head: doc.DocNode{ |
| 170 | name: 'db.sqlite' |
| 171 | } |
| 172 | }, |
| 173 | ] |
| 174 | toc := vd.gen_modules_toc('main') |
| 175 | assert !toc.contains('href="#"') |
| 176 | assert toc.contains('<div class="menu-row"><a>db</a></div>') |
| 177 | assert toc.contains('<li><a href="./db.mysql.html">mysql</a></li>') |
| 178 | } |
| 179 | |
| 180 | fn test_gen_modules_toc_uses_prefix_module_page_when_available() { |
| 181 | mut vd := VDoc{ |
| 182 | cfg: Config{ |
| 183 | is_multi: true |
| 184 | } |
| 185 | } |
| 186 | vd.docs = [ |
| 187 | doc.Doc{ |
| 188 | head: doc.DocNode{ |
| 189 | name: 'db.sqlite' |
| 190 | } |
| 191 | }, |
| 192 | doc.Doc{ |
| 193 | head: doc.DocNode{ |
| 194 | name: 'db' |
| 195 | } |
| 196 | }, |
| 197 | ] |
| 198 | toc := vd.gen_modules_toc('db') |
| 199 | assert toc.contains('<div class="menu-row"><a href="./db.html">db</a></div>') |
| 200 | } |
| 201 | |
| 202 | fn test_module_overview_uses_post_module_comment_without_readme() { |
| 203 | mod_dir := 'module_overview' |
| 204 | os.mkdir(mod_dir)! |
| 205 | os.write_file(os.join_path(mod_dir, 'overview.v'), "module overview |
| 206 | |
| 207 | // `overview` uses the first comment after the module declaration as the module overview. |
| 208 | |
| 209 | pub fn greet() string { |
| 210 | return 'hello' |
| 211 | } |
| 212 | ")! |
| 213 | res := os.execute_opt('${vexe_} doc -no-timestamp -f text -o - -readme -comments ${os.quoted_path( |
| 214 | './' + mod_dir)}') or { panic(err) } |
| 215 | assert res.exit_code == 0 |
| 216 | assert res.output.replace('\r\n', '\n').trim_space() == 'module overview |
| 217 | `overview` uses the first comment after the module declaration as the module overview. |
| 218 | |
| 219 | fn greet() string' |
| 220 | } |
| 221 | |
| 222 | fn test_html_keeps_enum_comment_after_top_level_comptime_if() { |
| 223 | mod_dir := 'issue_23338' |
| 224 | os.mkdir(mod_dir)! |
| 225 | os.write_file(os.join_path(mod_dir, 'issue_23338.v'), 'module issue_23338 |
| 226 | |
| 227 | \$if macos { |
| 228 | } |
| 229 | |
| 230 | // Foo lorem ipsum foo. |
| 231 | pub enum Foo { |
| 232 | foo |
| 233 | } |
| 234 | |
| 235 | // Bar ipsum lorem bar. |
| 236 | pub enum Bar { |
| 237 | bar |
| 238 | } |
| 239 | ')! |
| 240 | res := os.execute_opt('${vexe_} doc -no-timestamp -m -f html -o - -html-only-contents ${os.quoted_path( |
| 241 | './' + mod_dir)}') or { panic(err) } |
| 242 | assert res.exit_code == 0 |
| 243 | output := res.output.replace('\r\n', '\n') |
| 244 | assert output.contains('Foo lorem ipsum foo.') |
| 245 | assert output.contains('Bar ipsum lorem bar.') |
| 246 | } |
| 247 | |
| 248 | fn test_doc_generates_for_modules_without_public_symbols() { |
| 249 | mod_dir := 'module_without_public_symbols' |
| 250 | os.mkdir(mod_dir)! |
| 251 | os.write_file(os.join_path(mod_dir, 'module_without_public_symbols.v'), 'module module_without_public_symbols |
| 252 | |
| 253 | const internal = 1 |
| 254 | ')! |
| 255 | res := os.execute_opt('${vexe_} doc -no-timestamp -f text -o - ${os.quoted_path('./' + mod_dir)}') or { |
| 256 | panic(err) |
| 257 | } |
| 258 | assert res.exit_code == 0 |
| 259 | assert res.output.replace('\r\n', '\n').trim_space() == 'module module_without_public_symbols' |
| 260 | } |
| 261 | |
| 262 | fn test_resolve_relative_markdown_link() { |
| 263 | base := 'https://github.com/vlang/v/blob/master/vlib/net/html/' |
| 264 | assert resolve_relative_markdown_link(base, 'parser_test.v') == 'https://github.com/vlang/v/blob/master/vlib/net/html/parser_test.v' |
| 265 | assert resolve_relative_markdown_link(base, './html_test.v') == 'https://github.com/vlang/v/blob/master/vlib/net/html/html_test.v' |
| 266 | assert resolve_relative_markdown_link(base, '../README.md#usage') == 'https://github.com/vlang/v/blob/master/vlib/net/README.md#usage' |
| 267 | } |
| 268 | |
| 269 | fn test_resolve_relative_markdown_link_keeps_absolute_urls() { |
| 270 | base := 'https://github.com/vlang/v/blob/master/vlib/net/html/' |
| 271 | assert resolve_relative_markdown_link(base, 'https://vlang.io') == 'https://vlang.io' |
| 272 | assert resolve_relative_markdown_link(base, '/rooted/path') == '/rooted/path' |
| 273 | assert resolve_relative_markdown_link(base, '#local') == '#local' |
| 274 | } |
| 275 | |
| 276 | fn test_markdown_renderer_resolves_relative_links() ! { |
| 277 | base := 'https://github.com/vlang/v/blob/master/vlib/net/html/' |
| 278 | mut renderer := markdown.HtmlRenderer{ |
| 279 | transformer: &MdHtmlCodeHighlighter{ |
| 280 | table: ast.new_table() |
| 281 | relative_link_base: base |
| 282 | } |
| 283 | } |
| 284 | out := markdown.render('More examples in [parser](parser_test.v).', mut renderer)! |
| 285 | assert out.contains('<a href="https://github.com/vlang/v/blob/master/vlib/net/html/parser_test.v">') |
| 286 | } |
| 287 | |
| 288 | fn test_prepare_markdown_for_html_preserves_blockquote_linebreaks() ! { |
| 289 | mut renderer := markdown.HtmlRenderer{ |
| 290 | transformer: &MdHtmlCodeHighlighter{ |
| 291 | table: ast.new_table() |
| 292 | } |
| 293 | } |
| 294 | out := markdown.render(prepare_markdown_for_html('> **Note**\n> line one\n> line two'), mut |
| 295 | renderer)! |
| 296 | assert out.contains('<blockquote>') |
| 297 | assert out.contains('<strong>Note</strong><br />line one<br />line two') |
| 298 | } |
| 299 | |
| 300 | fn test_prepare_markdown_for_html_skips_fenced_code_blocks() { |
| 301 | input := '```sh\n> prompt\n> next\n```' |
| 302 | assert prepare_markdown_for_html(input) == input |
| 303 | } |
| 304 | |
| 305 | fn test_markdown_renderer_preserves_wrapped_readme_markdown() ! { |
| 306 | input := '1. The basic atomic elements of this regex engine are the tokens.\n In a query string a simple character is a token.\n\n- The basic element **is the token not the sequence of symbols**,\n and the most simple token, is a single character.\n\n- `|` **the OR operator acts on tokens,** for example `abc|ebc` is not\n `abc` OR `ebc`.' |
| 307 | mut renderer := markdown.HtmlRenderer{ |
| 308 | transformer: &MdHtmlCodeHighlighter{ |
| 309 | table: ast.new_table() |
| 310 | } |
| 311 | } |
| 312 | out := markdown.render(prepare_markdown_for_html(input), mut renderer)! |
| 313 | assert !out.contains('tokens.In') |
| 314 | assert !out.contains('mostsimple') |
| 315 | assert !out.contains('not<code>abc</code>') |
| 316 | assert out.contains('tokens. In a query string a simple character is a token.') |
| 317 | assert out.contains('the most simple token') |
| 318 | assert out.contains('is not <code>abc</code> OR <code>ebc</code>') |
| 319 | } |
| 320 | |