1# Copyright (C) 2022 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import os 16import sys 17import unittest 18 19ROOT_DIR = os.path.dirname( 20 os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 21sys.path.append(os.path.join(ROOT_DIR)) 22 23from python.generators.sql_processing.docs_parse import Arg, parse_file 24 25 26class TestStdlib(unittest.TestCase): 27 28 def test_valid_table(self): 29 res = parse_file( 30 'foo/bar.sql', f''' 31-- First line. 32-- Second line. 33-- @column slice_id Id of slice. 34-- @column slice_name Name of slice. 35CREATE TABLE foo_table AS 36SELECT 1; 37 '''.strip()) 38 self.assertListEqual(res.errors, []) 39 40 table = res.table_views[0] 41 self.assertEqual(table.name, 'foo_table') 42 self.assertEqual(table.desc, 'First line.\n Second line.') 43 self.assertEqual(table.type, 'TABLE') 44 self.assertEqual( 45 table.cols, { 46 'slice_id': Arg(None, 'Id of slice.'), 47 'slice_name': Arg(None, 'Name of slice.'), 48 }) 49 50 def test_valid_function(self): 51 res = parse_file( 52 'foo/bar.sql', f''' 53-- First line. 54-- Second line. 55-- @arg utid INT Utid of thread. 56-- @arg name STRING String name. 57CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING) 58-- Exists. 59RETURNS BOOL 60AS 61SELECT 1; 62 '''.strip()) 63 self.assertListEqual(res.errors, []) 64 65 fn = res.functions[0] 66 self.assertEqual(fn.name, 'foo_fn') 67 self.assertEqual(fn.desc, 'First line.\n Second line.') 68 self.assertEqual( 69 fn.args, { 70 'utid': Arg('INT', 'Utid of thread.'), 71 'name': Arg('STRING', 'String name.'), 72 }) 73 self.assertEqual(fn.return_type, 'BOOL') 74 self.assertEqual(fn.return_desc, 'Exists.') 75 76 def test_valid_table_function(self): 77 res = parse_file( 78 'foo/bar.sql', f''' 79-- Table comment. 80-- @arg utid INT Utid of thread. 81-- @arg name STRING String name. 82-- @column slice_id Id of slice. 83-- @column slice_name Name of slice. 84CREATE PERFETTO FUNCTION foo_view_fn(utid INT, name STRING) 85RETURNS TABLE(slice_id INT, slice_name STRING) 86AS SELECT 1; 87 '''.strip()) 88 self.assertListEqual(res.errors, []) 89 90 fn = res.table_functions[0] 91 self.assertEqual(fn.name, 'foo_view_fn') 92 self.assertEqual(fn.desc, 'Table comment.') 93 self.assertEqual( 94 fn.args, { 95 'utid': Arg('INT', 'Utid of thread.'), 96 'name': Arg('STRING', 'String name.'), 97 }) 98 self.assertEqual( 99 fn.cols, { 100 'slice_id': Arg('INT', 'Id of slice.'), 101 'slice_name': Arg('STRING', 'Name of slice.'), 102 }) 103 104 def test_missing_module_name(self): 105 res = parse_file( 106 'foo/bar.sql', f''' 107-- Comment 108-- @column slice_id Id of slice. 109CREATE TABLE bar_table AS 110SELECT 1; 111 '''.strip()) 112 # Expecting an error: function prefix (bar) not matching module name (foo). 113 self.assertEqual(len(res.errors), 1) 114 115 # Checks that custom prefixes (cr for chrome/util) are allowed. 116 def test_custom_module_prefix(self): 117 res = parse_file( 118 'chrome/util/test.sql', f''' 119-- Comment 120CREATE PERFETTO TABLE cr_table( 121 -- Column. 122 x INT 123) AS 124SELECT 1; 125 '''.strip()) 126 self.assertListEqual(res.errors, []) 127 128 fn = res.table_views[0] 129 self.assertEqual(fn.name, 'cr_table') 130 self.assertEqual(fn.desc, 'Comment') 131 self.assertEqual(fn.cols, { 132 'x': Arg('INT', 'Column.'), 133 }) 134 135 # Checks that when custom prefixes (cr for chrome/util) are present, 136 # the full module name (chrome) is still accepted. 137 def test_custom_module_prefix_full_module_name(self): 138 res = parse_file( 139 'chrome/util/test.sql', f''' 140-- Comment 141CREATE PERFETTO TABLE chrome_table( 142 -- Column. 143 x INT 144) AS 145SELECT 1; 146 '''.strip()) 147 self.assertListEqual(res.errors, []) 148 149 fn = res.table_views[0] 150 self.assertEqual(fn.name, 'chrome_table') 151 self.assertEqual(fn.desc, 'Comment') 152 self.assertEqual(fn.cols, { 153 'x': Arg('INT', 'Column.'), 154 }) 155 156 # Checks that when custom prefixes (cr for chrome/util) are present, 157 # the incorrect prefixes (foo) are not accepted. 158 def test_custom_module_prefix_incorrect(self): 159 res = parse_file( 160 'chrome/util/test.sql', f''' 161-- Comment 162CREATE PERFETTO TABLE foo_table( 163 -- Column. 164 x INT 165) AS 166SELECT 1; 167 '''.strip()) 168 # Expecting an error: table prefix (foo) is not allowed for a given path 169 # (allowed: chrome, cr). 170 self.assertEqual(len(res.errors), 1) 171 172 # Checks that when custom prefixes (cr for chrome/util) are present, 173 # they do not apply outside of the path scope. 174 def test_custom_module_prefix_does_not_apply_outside(self): 175 res = parse_file( 176 'foo/bar.sql', f''' 177-- Comment 178CREATE PERFETTO TABLE cr_table( 179 -- Column. 180 x INT 181) AS 182SELECT 1; 183 '''.strip()) 184 # Expecting an error: table prefix (foo) is not allowed for a given path 185 # (allowed: foo). 186 self.assertEqual(len(res.errors), 1) 187 188 def test_common_does_not_include_module_name(self): 189 res = parse_file( 190 'common/bar.sql', f''' 191-- Comment. 192-- @column slice_id Id of slice. 193CREATE TABLE common_table AS 194SELECT 1; 195 '''.strip()) 196 # Expecting an error: functions in common/ should not have a module prefix. 197 self.assertEqual(len(res.errors), 1) 198 199 def test_cols_typo(self): 200 res = parse_file( 201 'foo/bar.sql', f''' 202-- Comment. 203-- 204-- @column slice_id2 Foo. 205-- @column slice_name Bar. 206CREATE TABLE bar_table AS 207SELECT 1; 208 '''.strip()) 209 # Expecting an error: column slice_id2 not found in the table. 210 self.assertEqual(len(res.errors), 1) 211 212 def test_cols_no_desc(self): 213 res = parse_file( 214 'foo/bar.sql', f''' 215-- Comment. 216-- 217-- @column slice_id 218-- @column slice_name Bar. 219CREATE TABLE bar_table AS 220SELECT 1; 221 '''.strip()) 222 # Expecting an error: column slice_id is missing a description. 223 self.assertEqual(len(res.errors), 1) 224 225 def test_args_typo(self): 226 res = parse_file( 227 'foo/bar.sql', f''' 228-- Comment. 229-- 230-- @arg utid2 INT Uint. 231-- @arg name STRING String name. 232CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING) 233-- Exists. 234RETURNS BOOL 235AS 236SELECT 1; 237 '''.strip()) 238 # Expecting 2 errors: 239 # - arg utid2 not found in the function (should be utid); 240 # - utid not documented. 241 self.assertEqual(len(res.errors), 2) 242 243 def test_args_no_desc(self): 244 res = parse_file( 245 'foo/bar.sql', f''' 246-- Comment. 247-- 248-- @arg utid INT 249-- @arg name STRING String name. 250CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING) 251-- Exists. 252RETURNS BOOL 253AS 254SELECT 1; 255 '''.strip()) 256 # Expecting 2 errors: 257 # - arg utid is missing a description; 258 # - arg utid is not documented. 259 self.assertEqual(len(res.errors), 2) 260 261 def test_ret_no_desc(self): 262 res = parse_file( 263 'foo/bar.sql', f''' 264-- Comment 265CREATE PERFETTO FUNCTION foo_fn() 266-- 267RETURNS BOOL 268AS 269SELECT TRUE; 270 '''.strip()) 271 # Expecting an error: return value is missing a description. 272 self.assertEqual(len(res.errors), 1) 273 274 def test_multiline_desc(self): 275 res = parse_file( 276 'foo/bar.sql', f''' 277-- This 278-- is 279-- 280-- a 281-- very 282-- 283-- long 284-- 285-- description. 286CREATE PERFETTO FUNCTION foo_fn() 287-- Exists. 288RETURNS BOOL 289AS 290SELECT 1; 291 '''.strip()) 292 self.assertListEqual(res.errors, []) 293 294 fn = res.functions[0] 295 self.assertEqual(fn.desc, 296 'This\n is\n\n a\n very\n\n long\n\n description.') 297 298 def test_multiline_arg_desc(self): 299 res = parse_file( 300 'foo/bar.sql', f''' 301-- Comment. 302-- 303-- @arg utid INT Uint 304-- spread 305-- 306-- across lines. 307-- @arg name STRING String name 308-- which spans across multiple lines 309-- inconsistently. 310CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING) 311-- Exists. 312RETURNS BOOL 313AS 314SELECT 1; 315 '''.strip()) 316 317 fn = res.functions[0] 318 self.assertEqual( 319 fn.args, { 320 'utid': 321 Arg('INT', 'Uint spread across lines.'), 322 'name': 323 Arg( 324 'STRING', 'String name which spans across multiple lines ' 325 'inconsistently.'), 326 }) 327 328 def test_function_name_style(self): 329 res = parse_file( 330 'foo/bar.sql', f''' 331-- Function comment. 332CREATE PERFETTO FUNCTION foo_SnakeCase() 333-- Exists. 334RETURNS BOOL 335AS 336SELECT 1; 337 '''.strip()) 338 # Expecting an error: function name should be using hacker_style. 339 self.assertEqual(len(res.errors), 1) 340 341 def test_table_with_schema(self): 342 res = parse_file( 343 'foo/bar.sql', f''' 344-- Table comment. 345CREATE PERFETTO TABLE foo_table( 346 -- Id of slice. 347 id INT 348) AS 349SELECT 1 as id; 350 '''.strip()) 351 self.assertListEqual(res.errors, []) 352 353 table = res.table_views[0] 354 self.assertEqual(table.name, 'foo_table') 355 self.assertEqual(table.desc, 'Table comment.') 356 self.assertEqual(table.type, 'TABLE') 357 self.assertEqual(table.cols, { 358 'id': Arg('INT', 'Id of slice.'), 359 }) 360 361 def test_perfetto_view_with_schema(self): 362 res = parse_file( 363 'foo/bar.sql', f''' 364-- View comment. 365CREATE PERFETTO VIEW foo_table( 366 -- Foo. 367 foo INT, 368 -- Bar. 369 bar STRING 370) AS 371SELECT 1; 372 '''.strip()) 373 self.assertListEqual(res.errors, []) 374 375 table = res.table_views[0] 376 self.assertEqual(table.name, 'foo_table') 377 self.assertEqual(table.desc, 'View comment.') 378 self.assertEqual(table.type, 'VIEW') 379 self.assertEqual(table.cols, { 380 'foo': Arg('INT', 'Foo.'), 381 'bar': Arg('STRING', 'Bar.'), 382 }) 383 384 def test_function_with_new_style_docs(self): 385 res = parse_file( 386 'foo/bar.sql', f''' 387-- Function foo. 388CREATE PERFETTO FUNCTION foo_fn( 389 -- Utid of thread. 390 utid INT, 391 -- String name. 392 name STRING) 393-- Exists. 394RETURNS BOOL 395AS 396SELECT 1; 397 '''.strip()) 398 self.assertListEqual(res.errors, []) 399 400 fn = res.functions[0] 401 self.assertEqual(fn.name, 'foo_fn') 402 self.assertEqual(fn.desc, 'Function foo.') 403 self.assertEqual( 404 fn.args, { 405 'utid': Arg('INT', 'Utid of thread.'), 406 'name': Arg('STRING', 'String name.'), 407 }) 408 self.assertEqual(fn.return_type, 'BOOL') 409 self.assertEqual(fn.return_desc, 'Exists.') 410 411 def test_function_returns_table_with_new_style_docs(self): 412 res = parse_file( 413 'foo/bar.sql', f''' 414-- Function foo. 415CREATE PERFETTO FUNCTION foo_fn( 416 -- Utid of thread. 417 utid INT) 418-- Impl comment. 419RETURNS TABLE( 420 -- Count. 421 count INT 422) 423AS 424SELECT 1; 425 '''.strip()) 426 self.assertListEqual(res.errors, []) 427 428 fn = res.table_functions[0] 429 self.assertEqual(fn.name, 'foo_fn') 430 self.assertEqual(fn.desc, 'Function foo.') 431 self.assertEqual(fn.args, { 432 'utid': Arg('INT', 'Utid of thread.'), 433 }) 434 self.assertEqual(fn.cols, { 435 'count': Arg('INT', 'Count.'), 436 }) 437 438 def test_function_with_new_style_docs_multiline_comment(self): 439 res = parse_file( 440 'foo/bar.sql', f''' 441-- Function foo. 442CREATE PERFETTO FUNCTION foo_fn( 443 -- Multi 444 -- line 445 -- 446 -- comment. 447 arg INT) 448-- Exists. 449RETURNS BOOL 450AS 451SELECT 1; 452 '''.strip()) 453 self.assertListEqual(res.errors, []) 454 455 fn = res.functions[0] 456 self.assertEqual(fn.name, 'foo_fn') 457 self.assertEqual(fn.desc, 'Function foo.') 458 self.assertEqual(fn.args, { 459 'arg': Arg('INT', 'Multi line comment.'), 460 }) 461 self.assertEqual(fn.return_type, 'BOOL') 462 self.assertEqual(fn.return_desc, 'Exists.') 463 464 def test_function_with_multiline_return_comment(self): 465 res = parse_file( 466 'foo/bar.sql', f''' 467-- Function foo. 468CREATE PERFETTO FUNCTION foo_fn( 469 -- Arg 470 arg INT) 471-- Multi 472-- line 473-- return 474-- comment. 475RETURNS BOOL 476AS 477SELECT 1; 478 '''.strip()) 479 self.assertListEqual(res.errors, []) 480 481 fn = res.functions[0] 482 self.assertEqual(fn.name, 'foo_fn') 483 self.assertEqual(fn.desc, 'Function foo.') 484 self.assertEqual(fn.args, { 485 'arg': Arg('INT', 'Arg'), 486 }) 487 self.assertEqual(fn.return_type, 'BOOL') 488 self.assertEqual(fn.return_desc, 'Multi line return comment.') 489 490 def test_create_or_replace_table_banned(self): 491 res = parse_file( 492 'common/bar.sql', f''' 493-- Table. 494CREATE OR REPLACE PERFETTO TABLE foo( 495 -- Column. 496 x INT 497) 498AS 499SELECT 1; 500 501 '''.strip()) 502 # Expecting an error: CREATE OR REPLACE is not allowed in stdlib. 503 self.assertEqual(len(res.errors), 1) 504 505 def test_create_or_replace_view_banned(self): 506 res = parse_file( 507 'common/bar.sql', f''' 508-- Table. 509CREATE OR REPLACE PERFETTO VIEW foo( 510 -- Column. 511 x INT 512) 513AS 514SELECT 1; 515 516 '''.strip()) 517 # Expecting an error: CREATE OR REPLACE is not allowed in stdlib. 518 self.assertEqual(len(res.errors), 1) 519 520 def test_create_or_replace_function_banned(self): 521 res = parse_file( 522 'foo/bar.sql', f''' 523-- Function foo. 524CREATE OR REPLACE PERFETTO FUNCTION foo_fn() 525-- Exists. 526RETURNS BOOL 527AS 528SELECT 1; 529 '''.strip()) 530 # Expecting an error: CREATE OR REPLACE is not allowed in stdlib. 531 self.assertEqual(len(res.errors), 1) 532 533 def test_function_with_new_style_docs_with_parenthesis(self): 534 res = parse_file( 535 'foo/bar.sql', f''' 536-- Function foo. 537CREATE PERFETTO FUNCTION foo_fn( 538 -- Utid of thread (important). 539 utid INT) 540-- Exists. 541RETURNS BOOL 542AS 543SELECT 1; 544 '''.strip()) 545 self.assertListEqual(res.errors, []) 546 547 fn = res.functions[0] 548 self.assertEqual(fn.name, 'foo_fn') 549 self.assertEqual(fn.desc, 'Function foo.') 550 self.assertEqual(fn.args, { 551 'utid': Arg('INT', 'Utid of thread (important).'), 552 }) 553 self.assertEqual(fn.return_type, 'BOOL') 554 self.assertEqual(fn.return_desc, 'Exists.') 555 556 def test_macro(self): 557 res = parse_file( 558 'foo/bar.sql', f''' 559-- Macro 560CREATE OR REPLACE PERFETTO FUNCTION foo_fn() 561-- Exists. 562RETURNS BOOL 563AS 564SELECT 1; 565 '''.strip()) 566 # Expecting an error: CREATE OR REPLACE is not allowed in stdlib. 567 self.assertEqual(len(res.errors), 1) 568 569 def test_create_or_replace_macro_smoke(self): 570 res = parse_file( 571 'foo/bar.sql', f''' 572-- Macro 573CREATE PERFETTO MACRO foo_macro( 574 -- x Arg. 575 x TableOrSubquery 576) 577-- Exists. 578RETURNS TableOrSubquery 579AS 580SELECT 1; 581 '''.strip()) 582 583 macro = res.macros[0] 584 self.assertEqual(macro.name, 'foo_macro') 585 self.assertEqual(macro.desc, 'Macro') 586 self.assertEqual(macro.args, { 587 'x': Arg('TableOrSubquery', 'x Arg.'), 588 }) 589 self.assertEqual(macro.return_type, 'TableOrSubquery') 590 self.assertEqual(macro.return_desc, 'Exists.') 591 592 def test_create_or_replace_macro_banned(self): 593 res = parse_file( 594 'foo/bar.sql', f''' 595-- Macro 596CREATE OR REPLACE PERFETTO MACRO foo_macro( 597 -- x Arg. 598 x TableOrSubquery 599) 600-- Exists. 601RETURNS TableOrSubquery 602AS 603SELECT 1; 604 '''.strip()) 605 # Expecting an error: CREATE OR REPLACE is not allowed in stdlib. 606 self.assertEqual(len(res.errors), 1) 607