├── .github └── workflows │ └── main.yml ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── Steepfile ├── bin └── rgot ├── go ├── README.md ├── go.mod ├── go_bar_test.go ├── go_benchmark_test.go ├── go_foo_test.go └── go_fuzzing_test.go ├── lib ├── rgot.rb └── rgot │ ├── b.rb │ ├── benchmark_result.rb │ ├── cli.rb │ ├── common.rb │ ├── example_parser.rb │ ├── f.rb │ ├── m.rb │ ├── pb.rb │ ├── t.rb │ └── version.rb ├── rbs_collection.lock.yaml ├── rbs_collection.yaml ├── rgot.gemspec ├── sig ├── patch.rbs └── rgot.rbs └── test ├── bar ├── bar_test.rb └── baz │ └── baz_test.rb ├── benchmark_test.rb ├── example_fail_test.rb ├── example_pass_test.rb ├── fail_test.rb ├── fatal_test.rb ├── foo └── foo_test.rb ├── fuzzing_fail_test.rb ├── fuzzing_multi_test.rb ├── fuzzing_pass_test.rb ├── main_test.rb ├── multi_module_fail_test.rb ├── multi_module_pass_test.rb ├── pass_test.rb ├── rgot_benchmark_test.rb ├── rgot_common_test.rb ├── rgot_example_test.rb ├── rgot_fuzzing_test.rb ├── rgot_test.rb ├── sample.rb ├── skip_test.rb └── timeout_test.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.2' 18 | - '3.3' 19 | - '3.4' 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | bundler-cache: true 28 | - name: Run the default task 29 | run: bundle exec rake 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | tmp 3 | /.gem_rbs_collection 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "rbs" 6 | gem "steep" 7 | gem "debug" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rgot (1.5.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activesupport (8.0.1) 10 | base64 11 | benchmark (>= 0.3) 12 | bigdecimal 13 | concurrent-ruby (~> 1.0, >= 1.3.1) 14 | connection_pool (>= 2.2.5) 15 | drb 16 | i18n (>= 1.6, < 2) 17 | logger (>= 1.4.2) 18 | minitest (>= 5.1) 19 | securerandom (>= 0.3) 20 | tzinfo (~> 2.0, >= 2.0.5) 21 | uri (>= 0.13.1) 22 | ast (2.4.2) 23 | base64 (0.2.0) 24 | benchmark (0.4.0) 25 | bigdecimal (3.1.9) 26 | concurrent-ruby (1.3.5) 27 | connection_pool (2.5.0) 28 | csv (3.3.2) 29 | date (3.4.1) 30 | debug (1.10.0) 31 | irb (~> 1.10) 32 | reline (>= 0.3.8) 33 | drb (2.2.1) 34 | ffi (1.17.1) 35 | fileutils (1.7.3) 36 | i18n (1.14.7) 37 | concurrent-ruby (~> 1.0) 38 | io-console (0.8.0) 39 | irb (1.15.1) 40 | pp (>= 0.6.0) 41 | rdoc (>= 4.0.0) 42 | reline (>= 0.4.2) 43 | json (2.10.1) 44 | language_server-protocol (3.17.0.4) 45 | listen (3.9.0) 46 | rb-fsevent (~> 0.10, >= 0.10.3) 47 | rb-inotify (~> 0.9, >= 0.9.10) 48 | logger (1.6.6) 49 | minitest (5.25.4) 50 | parser (3.3.7.1) 51 | ast (~> 2.4.1) 52 | racc 53 | pp (0.6.2) 54 | prettyprint 55 | prettyprint (0.2.0) 56 | psych (5.2.3) 57 | date 58 | stringio 59 | racc (1.8.1) 60 | rainbow (3.1.1) 61 | rake (13.2.1) 62 | rb-fsevent (0.11.2) 63 | rb-inotify (0.11.1) 64 | ffi (~> 1.0) 65 | rbs (3.8.1) 66 | logger 67 | rdoc (6.12.0) 68 | psych (>= 4.0.0) 69 | reline (0.6.0) 70 | io-console (~> 0.5) 71 | securerandom (0.4.1) 72 | steep (1.9.4) 73 | activesupport (>= 5.1) 74 | concurrent-ruby (>= 1.1.10) 75 | csv (>= 3.0.9) 76 | fileutils (>= 1.1.0) 77 | json (>= 2.1.0) 78 | language_server-protocol (>= 3.15, < 4.0) 79 | listen (~> 3.0) 80 | logger (>= 1.3.0) 81 | parser (>= 3.1) 82 | rainbow (>= 2.2.2, < 4.0) 83 | rbs (~> 3.8) 84 | securerandom (>= 0.1) 85 | strscan (>= 1.0.0) 86 | terminal-table (>= 2, < 4) 87 | uri (>= 0.12.0) 88 | stringio (3.1.5) 89 | strscan (3.1.2) 90 | terminal-table (3.0.2) 91 | unicode-display_width (>= 1.1.1, < 3) 92 | tzinfo (2.0.6) 93 | concurrent-ruby (~> 1.0) 94 | unicode-display_width (2.6.0) 95 | uri (1.0.3) 96 | 97 | PLATFORMS 98 | ruby 99 | 100 | DEPENDENCIES 101 | bundler 102 | debug 103 | rake 104 | rbs 105 | rgot! 106 | steep 107 | 108 | BUNDLED WITH 109 | 2.4.2 110 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 ksss 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rgot 2 | === 3 | 4 | [![Ruby](https://github.com/ksss/rgot/actions/workflows/main.yml/badge.svg)](https://github.com/ksss/rgot/actions/workflows/main.yml) 5 | 6 | Ruby + Golang Testing = Rgot 7 | 8 | Rgot is a testing package convert from golang testing. 9 | 10 | ### Usage 11 | 12 | test/sample.rb 13 | 14 | ```ruby 15 | class Sample 16 | def sum(i, j) 17 | i + j 18 | end 19 | end 20 | ``` 21 | 22 | test/pass_test.rb 23 | 24 | ```ruby 25 | module SampleTest 26 | class TypeSum < Struct.new(:left, :right, :expect) 27 | end 28 | 29 | DATA = [ 30 | TypeSum.new(2, 3, 5), 31 | TypeSum.new(12, 9, 21), 32 | TypeSum.new(85, 42, 127), 33 | ] 34 | 35 | def test_pass(t) 36 | s = Sample.new 37 | DATA.each do |ts| 38 | sum = s.sum(ts.left, ts.right) 39 | unless sum.kind_of?(Integer) 40 | t.error("expect Integer got #{sum.class}") 41 | end 42 | unless sum == ts.expect 43 | t.error("expect #{ts.expect} got #{sum}") 44 | end 45 | end 46 | end 47 | end 48 | ``` 49 | 50 | ``` 51 | $ rgot -v --require ./test/sample test/pass_test.rb 52 | === RUN test_pass 53 | --- PASS: test_pass (0.00003s) 54 | PASS 55 | ok 0.001s 56 | ``` 57 | 58 | # Features 59 | 60 | ## Testing 61 | 62 | I provide a very simple testing feature to you. 63 | 64 | **Rgot** testing is quite different from *RSpec* and *MiniTest* etc. 65 | 66 | Rgot carve out a new world of testing. 67 | 68 | So, You check only bad case in testing. 69 | 70 | ## Benchmark 71 | 72 | You can write simple benchmark script with testing. 73 | 74 | This benchmark to adjust the time automatically. 75 | 76 | ```ruby 77 | module FooTest 78 | def benchmark_something(b) 79 | i = 0 80 | while i < b.n 81 | something(1) 82 | i += 1 83 | end 84 | end 85 | end 86 | ``` 87 | 88 | ``` 89 | $ rgot foo_test.rb --bench . 90 | benchmark_something 14400000 81 ns/op 91 | ok FooTest 2.782s 92 | ``` 93 | 94 | `b.n` is automatically adjusted. 95 | 96 | ## Fuzzing 97 | 98 | ``` 99 | $ rgot target_file_test.rb --fuzz . --fuzztime 1 100 | ``` 101 | 102 | Fuzzing tests are also supported. 103 | Please refer to the gloang documentation for details. 104 | 105 | https://go.dev/security/fuzz/ 106 | 107 | ```ruby 108 | module FooTest 109 | # To enable fuzzing, the method name 110 | # should be prefixed with `fuzz`. 111 | def fuzz_any_func(f) 112 | f.add(5, "hello") 113 | f.fuzz do |t, i, s| 114 | out, err = foo(i, s) 115 | if err != nil && out != "" 116 | t.errorf("%s, %s", out, err) 117 | end 118 | end 119 | end 120 | end 121 | ``` 122 | 123 | ## Example 124 | 125 | Rgot's example feature is the best and if you want to write the sample code of your library. 126 | 127 | While presenting the sample code, it will be able to test whether the display results match at the same time. 128 | 129 | ```ruby 130 | module FooTest 131 | class User 132 | def initialize(name) 133 | @name = name 134 | end 135 | 136 | def hello 137 | "Hello #{@name}" 138 | end 139 | end 140 | 141 | def example_something 142 | user = User.new('ksss') 143 | puts user.hello 144 | # Output: 145 | # Hello ksss 146 | end 147 | 148 | def example_fail 149 | user = User.new('ksss') 150 | puts user.hello 151 | # Output: 152 | # Hi ksss 153 | end 154 | end 155 | ``` 156 | 157 | `example_fail` fail since output is different. 158 | 159 | So, you can notice that the sample code is wrong. 160 | 161 | # Table Driven Tests 162 | 163 | ```rb 164 | FLAGTESTS = [ 165 | ["%a", "[%a]"], 166 | ["%-a", "[%-a]"], 167 | ["%+a", "[%+a]"], 168 | ["%#a", "[%#a]"], 169 | ["% a", "[% a]"], 170 | ["%0a", "[%0a]"], 171 | ["%1.2a", "[%1.2a]"], 172 | ["%-1.2a", "[%-1.2a]"], 173 | ["%+1.2a", "[%+1.2a]"], 174 | ["%-+1.2a", "[%+-1.2a]"], 175 | ["%-+1.2abc", "[%+-1.2a]bc"], 176 | ["%-1.2abc", "[%-1.2a]bc"], 177 | ] 178 | 179 | def test_flag_parser(t) 180 | FLAGTESTS.each do |input, output| 181 | s = Flag.print(input) 182 | unless s == output 183 | t.errorf("Flag.print(%p) => %p, want %p", input, s, output) 184 | end 185 | end 186 | end 187 | ``` 188 | 189 | see https://github.com/golang/go/wiki/TableDrivenTests 190 | 191 | # Naming convention 192 | 193 | ## Filename 194 | 195 | Filename should be set '*_test.rb' 196 | 197 | ## Module name 198 | 199 | Module name should be set 'XxxTest' 200 | 201 | 'Xxx' can replace any string (in range of ruby module) 202 | 203 | Testing code file can split any number. 204 | 205 | But all file should be have one module (like golang package name). 206 | 207 | ```ruby 208 | module XxxTest 209 | # ... 210 | end 211 | ``` 212 | 213 | ## Method name 214 | 215 | Method name should be set `test_*` for testing. 216 | 217 | And benchmark method should be set `benchmark_*`. 218 | 219 | And fuzz method should be set `fuzz_*`. 220 | 221 | And example method should be set `example_*`. 222 | 223 | ```ruby 224 | module XxxTest 225 | def test_any_name(t) 226 | end 227 | 228 | def benchmark_any_name(b) 229 | end 230 | 231 | def fuzz_any_name(f) 232 | end 233 | 234 | def example_any_name 235 | end 236 | end 237 | ``` 238 | 239 | # Command line interface 240 | 241 | ``` 242 | $ rgot -h 243 | Usage: rgot [options] 244 | -v, --verbose log all tests 245 | --version show Rgot version 246 | --bench [regexp] benchmark 247 | --benchtime [sec] benchmark running time 248 | --timeout [sec] set timeout sec to testing 249 | --cpu [count,...] set cpu counts of comma split 250 | --thread [count,...] set thread counts of comma split 251 | --require [path] load some code before running 252 | --load-path [path] Specify $LOAD_PATH directory 253 | --fuzz [regexp] run the fuzz test matching `regexp` 254 | --fuzztime [sec] time to spend fuzzing; default is to run indefinitely 255 | ``` 256 | 257 | ## Basic 258 | 259 | ``` 260 | $ rgot file_of_test.rb 261 | PASS 262 | ok 0.001s 263 | ``` 264 | 265 | Set filename to argument. 266 | 267 | Just only start testing file_of_test.rb. 268 | 269 | ``` 270 | $ rgot sample 271 | PASS 272 | ok 0.002s 273 | ``` 274 | 275 | And set dirname to argument, run all case of testing under this dir. 276 | 277 | ## Verbose 278 | 279 | ``` 280 | $ rgot -v target_file_test.rb 281 | === RUN test_pass 282 | --- PASS: test_pass (0.00005s) 283 | PASS 284 | ok 0.001s 285 | ``` 286 | 287 | Show all log and more detail information of testing. 288 | 289 | ## Benchmark 290 | 291 | ``` 292 | $ rgot target_file_test.rb --bench . 293 | ``` 294 | 295 | Run testing with benchmark. 296 | 297 | `.` means match all string for regexp. 298 | 299 | Set `someone` if you only run benchmark to match `someone` method.(e.g. benchmark_someone_1) 300 | 301 | ### Parallel benchmark 302 | 303 | Benchmark for parallel performance. 304 | 305 | `--cpu` option set process counts (default `Etc.nprocessors`). 306 | 307 | And `--thread` option set thread counts (default 1). 308 | 309 | Benchmark fork, run and report each by process counts. 310 | 311 | (**process** and **thread** means ruby/linux native process and thread) 312 | 313 | ```ruby 314 | module FooTest 315 | def benchmark_any_func(b) 316 | b.run_parallel do |pb| 317 | # pb is instance of Rgot::PB 318 | # call some time by b.n 319 | while pb.next 320 | some_func() 321 | end 322 | end 323 | end 324 | end 325 | ``` 326 | 327 | ``` 328 | $ rgot foo_test.rb --bench . --cpu=2,4 --thread=2,4 329 | benchmark_any_func-2(2) 40 13363604 ns/op 330 | benchmark_any_func-2(4) 160 7125845 ns/op 331 | benchmark_any_func-4(2) 160 7224815 ns/op 332 | benchmark_any_func-4(4) 320 3652431 ns/op 333 | ok FooTest 3.061s 334 | ``` 335 | 336 | ## Timeout 337 | 338 | ``` 339 | $ rgot target_file_test.rb --timeout 3 340 | ``` 341 | 342 | You can set timeout sec for testing (default 0). 343 | 344 | Fail testing and print raised exception message to STDERR if timeout. 345 | 346 | # Recommendation 347 | 348 | Set up Rakefile. 349 | 350 | ```rb 351 | # Rakefile 352 | require "rake/testtask" 353 | Rake::TestTask.new do |task| 354 | task.libs = %w[lib test] 355 | task.test_files = FileList["lib/**/*_test.rb"] 356 | end 357 | ``` 358 | 359 | Set `test/test_helper.rb`. 360 | 361 | ```rb 362 | # test/test_helper.rb 363 | require "rgot/cli" 364 | 365 | unless $PROGRAM_NAME.end_with?("/rgot") 366 | at_exit do 367 | exit Rgot::Cli.new(["-v", "lib"]).run 368 | end 369 | end 370 | ``` 371 | 372 | Place the test file in the same directory as the implementation file. 373 | Just like in golang. 374 | 375 | ```console 376 | $ ls lib 377 | lib/foo.rb 378 | lib/foo_test.rb 379 | ``` 380 | 381 | Write your testing code. 382 | 383 | ```rb 384 | # lib/foo_test.rb 385 | require 'test_helper' 386 | 387 | module FooTest 388 | def test_foo(t) 389 | # ... 390 | end 391 | end 392 | ``` 393 | 394 | OK, You will be able to both run all tests with rake and specify one file to run. 395 | 396 | ```console 397 | $ bundle exec rake test 398 | ``` 399 | 400 | ```console 401 | $ bundle exec rgot lib/foo_test.rb 402 | ``` 403 | 404 | # Methods 405 | 406 | ## Rgot 407 | 408 | ### Rgot.benchmark 409 | 410 | Run benchmark function without framework. 411 | 412 | ```ruby 413 | result = Rgot.benchmark do |b| 414 | i = 0 415 | while i < b.n 416 | some_func() 417 | i += 1 418 | end 419 | end 420 | puts result #=> 100000 100 ns/op 421 | ``` 422 | 423 | ### Rgot.verbose? 424 | 425 | Check running with option verbose true/false. 426 | 427 | ## Rgot::M (Main) 428 | 429 | Main method run first on testing. 430 | 431 | And this is default virtual main code. 432 | 433 | ```ruby 434 | module TestSomeCode 435 | def test_main(m) 436 | m.run 437 | end 438 | end 439 | ``` 440 | 441 | Main method should be set 'test_main' only. 442 | 443 | Variable `m` is a instance of `Rgot::M` class means Main. 444 | 445 | `Rgot::M#run` start all testing methods. 446 | 447 | And return code of process end status. 448 | 449 | If you want to run before/after all testing method, You can write like this. 450 | 451 | ```ruby 452 | module TestSomeCode 453 | def test_main(m) 454 | the_before_running_some_code 455 | code = m.run 456 | the_after_running_some_code 457 | code 458 | end 459 | end 460 | ``` 461 | 462 | ## Rgot::Common 463 | 464 | `Rgot::Common` is inherited to `Rgot::T` and `Rgot::B` 465 | 466 | `Rgot::Common` have some logging method. 467 | 468 | ### Rgot::Common#log 469 | 470 | ```ruby 471 | t.log("wooooo", 1, 2, 3) 472 | ``` 473 | 474 | Write any log message. 475 | 476 | But this message to show need -v option. 477 | 478 | ### Rgot::Common#logf 479 | 480 | Write any log message like sprintf. 481 | 482 | ```ruby 483 | t.logf("%d-%s", 10, "foo") 484 | ``` 485 | 486 | ### Rgot::Common#error 487 | 488 | ```ruby 489 | t.error("expect #{a} got #{b}") 490 | ``` 491 | 492 | Test fail and show some error message. 493 | 494 | ### Rgot::Common#errorf 495 | 496 | Fail loggin same as logf 497 | 498 | ### Rgot::Common#fatal 499 | 500 | Testing stop and fail with log. 501 | 502 | ```ruby 503 | t.fatal("fatal error!") 504 | ``` 505 | 506 | ### Rgot::Common#fatalf 507 | 508 | Fatal logging same as logf 509 | 510 | ### Rgot::Common#skip 511 | 512 | ```ruby 513 | t.skip("this method was skipped") 514 | ``` 515 | 516 | Skip current testing method. 517 | 518 | And run to next testing method. 519 | 520 | ### Rgot::Common#skipf 521 | 522 | Skip logging same as logf 523 | 524 | ## Rgot::T (Testing) 525 | 526 | Testing is a main usage of this package. 527 | 528 | ```ruby 529 | module TestSomeCode 530 | def test_some_1(t) 531 | end 532 | end 533 | ``` 534 | 535 | The `t` variable is instance of `Rgot::T` class means Testing. 536 | 537 | ## Rgot::B (Benchmark) 538 | 539 | For Benchmark class. 540 | 541 | Can use log methods same as `Rgot::T` class 542 | 543 | ### Rgot::B#n 544 | 545 | Automatic number calculated by running time. 546 | 547 | Recommend to this idiom. 548 | 549 | ```ruby 550 | def benchmark_something(b) 551 | i = 0 552 | while i < b.n 553 | something() 554 | i += 1 555 | end 556 | end 557 | ``` 558 | 559 | ### Rgot::B#reset_timer 560 | 561 | Reset benchmark timer 562 | 563 | ```ruby 564 | def benchmark_something(b) 565 | obj = heavy_prepare_method() 566 | b.reset_timer # you can ignore time of havy_prepare_method() 567 | i = 0 568 | while i < b.n 569 | obj.something() 570 | i += 1 571 | end 572 | end 573 | ``` 574 | 575 | ### Rgot::B#start_timer 576 | 577 | Start benchmark timer 578 | 579 | ### Rgot::B#stop_timer 580 | 581 | Stop benchmark timer 582 | 583 | ### Rgot::B#run_parallel 584 | 585 | Start parallel benchmark using `fork` and `Thread.new`. 586 | 587 | This method should be call with block. 588 | 589 | The block argument is instance of Rgot::PB. 590 | 591 | ## Rgot::PB (Parallel Benchmark) 592 | 593 | ### Rgot::PB#next 594 | 595 | Should be call this when parallel benchmark. 596 | 597 | Repeat while return false. 598 | 599 | Recommend this idiom. 600 | 601 | ```ruby 602 | def benchmark_foo(b) 603 | b.run_parallel do |pb| 604 | while pb.next 605 | some_func() 606 | end 607 | end 608 | end 609 | ``` 610 | 611 | ## Rgot::F (Fuzzing) 612 | 613 | ### Rgot::F#add 614 | 615 | Set the sample value with `#add`. This value is also used as a test. It guesses the type from the value and generates a random value. 616 | 617 | ### Rgot::F#fuzz 618 | 619 | Generate the random value generated by `#fuzz` and execute the code. 620 | The `t` becomes an instance of `Rgot::T` and the test can be run as usual. 621 | 622 | ```ruby 623 | def fuzz_foo(f) 624 | f.add(100, "hello") 625 | f.fuzz do |t, i, s| 626 | i #=> 100, 84, 17, 9, 66, ... 627 | s #=> "hello", "Y\xD5\xAB\xBA\x8E", "r\x95D\xA5\xF7", "\xCEj=\x9C\xBD", ... 628 | if !foo(i, s) 629 | t.error("fail with i=#{i}, s=#{s}") 630 | end 631 | end 632 | end 633 | ``` 634 | 635 | # TODO 636 | 637 | - [ ] Support to save and load fuzzing data 638 | 639 | ## v2 640 | 641 | - [ ] Support sub testing 642 | - [ ] Fix duration argument unit 643 | - [ ] Refactoring 644 | - [ ] Fix M#initialize argument 645 | - [ ] Fix internal class API 646 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | desc "test to rgot" 4 | task :test do |t| 5 | targets = [ 6 | "test/rgot_common_test.rb", 7 | "test/rgot_test.rb", 8 | "test/rgot_benchmark_test.rb", 9 | "test/rgot_example_test.rb", 10 | "test/rgot_fuzzing_test.rb", 11 | ] 12 | ruby "bin/rgot -v #{targets.join(' ')}" 13 | end 14 | 15 | task :default => [:test] 16 | -------------------------------------------------------------------------------- /Steepfile: -------------------------------------------------------------------------------- 1 | target :lib do 2 | check "lib" 3 | signature "sig" 4 | end 5 | -------------------------------------------------------------------------------- /bin/rgot: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | require 'rgot/cli' 3 | 4 | exit Rgot::Cli.new(ARGV).run 5 | -------------------------------------------------------------------------------- /go/README.md: -------------------------------------------------------------------------------- 1 | This code is just to check the behavior of golang. 2 | 3 | You can run following command on this dir. 4 | 5 | ``` 6 | $ go test -v 7 | ``` 8 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ksss/rgot 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /go/go_bar_test.go: -------------------------------------------------------------------------------- 1 | package t_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBar1(t *testing.T) { 8 | // t.Error("bar1") 9 | } 10 | 11 | func TestBar2(t *testing.T) { 12 | // t.Error("bar2") 13 | } 14 | -------------------------------------------------------------------------------- /go/go_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package t_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func Benchmark1(b *testing.B) { 9 | b.Log(b) 10 | for i := 0; i < b.N; i++ { 11 | time.Sleep(1 * time.Millisecond) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /go/go_foo_test.go: -------------------------------------------------------------------------------- 1 | package t_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFoo1(t *testing.T) { 8 | t.Run("AAAAA", func(t *testing.T) { 9 | t.Log("Sub log") 10 | }) 11 | } 12 | 13 | func TestFoo2(t *testing.T) { 14 | // t.Error("foo2") 15 | } 16 | -------------------------------------------------------------------------------- /go/go_fuzzing_test.go: -------------------------------------------------------------------------------- 1 | package t_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Foo(i int, s string) (string, error) { 8 | return "", nil 9 | } 10 | 11 | func FuzzPass(f *testing.F) { 12 | f.Add(5, "hello") 13 | f.Fuzz(func(t *testing.T, i int, s string) { 14 | // t.Error("fuzz!") 15 | }) 16 | } 17 | 18 | func FuzzFail(f *testing.F) { 19 | f.Add(5, "hello") 20 | f.Fuzz(func(t *testing.T, i int, s string) { 21 | // t.Error("fuzz!") 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /lib/rgot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rgot 4 | autoload :VERSION, 'rgot/version' 5 | autoload :Common, 'rgot/common' 6 | autoload :M, 'rgot/m' 7 | autoload :T, 'rgot/t' 8 | autoload :B, 'rgot/b' 9 | autoload :PB, 'rgot/pb' 10 | autoload :BenchmarkResult, 'rgot/benchmark_result' 11 | autoload :F, 'rgot/f' 12 | autoload :ExampleParser, 'rgot/example_parser' 13 | 14 | OptionError = Class.new(StandardError) 15 | InternalTest = Struct.new(:module, :name) 16 | InternalBenchmark = Struct.new(:module, :name) 17 | InternalExample = Struct.new(:module, :name) 18 | InternalFuzzTarget = Struct.new(:module, :name) 19 | ExampleOutput = Struct.new(:name, :output) 20 | 21 | class << self 22 | def now 23 | Process.clock_gettime(Process::CLOCK_MONOTONIC) 24 | end 25 | 26 | def benchmark(opts_hash = {}, &block) 27 | opts = B::Options.new 28 | opts_hash.each do |k, v| 29 | opts[k] = v 30 | end 31 | B.new(nil, nil, opts).run(&block) 32 | end 33 | 34 | def verbose? 35 | @chatty 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/rgot/b.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rgot 4 | class B < Common 5 | Options = _ = Struct.new( 6 | :procs, 7 | :threads, 8 | :benchtime, 9 | ) 10 | 11 | # @dynamic n, n= 12 | attr_accessor :n 13 | 14 | def initialize(benchmark_module, name, opts = Options.new) 15 | super() 16 | @n = 1 17 | @module = benchmark_module 18 | @name = name 19 | @opts = opts 20 | @timer_on = false 21 | @duration = 0.0 22 | @module.extend @module if @module 23 | end 24 | 25 | def start_timer 26 | if !@timer_on 27 | @start = Rgot.now 28 | @timer_on = true 29 | end 30 | end 31 | 32 | def stop_timer 33 | if @timer_on 34 | @duration += Rgot.now - @start 35 | @timer_on = false 36 | end 37 | end 38 | 39 | def reset_timer 40 | if @timer_on 41 | @start = Rgot.now 42 | end 43 | @duration = 0.0 44 | end 45 | 46 | def run(&block) 47 | n = 1 48 | benchtime = (@opts.benchtime || 1).to_f 49 | catch(:skip) do 50 | run_n(n, block) 51 | while !failed? && @duration < benchtime && n < 1e9 52 | if @duration < (benchtime / 100.0) 53 | n *= 100 54 | elsif @duration < (benchtime / 10.0) 55 | n *= 10 56 | elsif @duration < (benchtime / 5.0) 57 | n *= 5 58 | elsif @duration < (benchtime / 2.0) 59 | n *= 2 60 | else 61 | if n == 1 62 | break 63 | end 64 | n = [(n * 1.2).to_i, n + 1].max || raise 65 | end 66 | run_n(n, block) 67 | end 68 | end 69 | 70 | BenchmarkResult.new(n: n, t: @duration) 71 | end 72 | 73 | def run_parallel 74 | raise LocalJumpError, "no block given" unless block_given? 75 | 76 | procs = (@opts.procs || 1) 77 | threads = (@opts.threads || 1) 78 | 79 | procs.times do 80 | fork do 81 | Array.new(threads) { 82 | Thread.new { 83 | yield PB.new(bn: @n) 84 | }.tap { |t| t.abort_on_exception = true } 85 | }.each(&:join) 86 | end 87 | end 88 | Process.waitall 89 | end 90 | 91 | private 92 | 93 | def run_n(n, block = nil) 94 | GC.start 95 | @n = n 96 | reset_timer 97 | start_timer 98 | if block 99 | block.call(self) 100 | else 101 | bench_method = @module.instance_method(@name).bind(@module) 102 | if bench_method.arity == 0 103 | path, line = bench_method.source_location 104 | skip "#{path}:#{line} `#{bench_method.name}' is not running. It's a benchmark method name, But not have argument" 105 | else 106 | bench_method.call(self) 107 | end 108 | end 109 | stop_timer 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/rgot/benchmark_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rgot 4 | class BenchmarkResult 5 | # @dynamic n, t 6 | attr_reader :n 7 | attr_reader :t 8 | 9 | def initialize(n:, t:) 10 | @n = n 11 | @t = t 12 | end 13 | 14 | def to_s 15 | sprintf("%d\t%d ns/op", @n, @t / @n * 1_000_000_000) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rgot/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'optparse' 4 | 5 | require_relative '../rgot' 6 | 7 | module Rgot 8 | class Cli 9 | def initialize(argv) 10 | @argv = argv 11 | end 12 | 13 | def run 14 | opts = Rgot::M::Options.new 15 | parse_option(opts) 16 | main_process(opts) 17 | end 18 | 19 | private 20 | 21 | def parse_option(opts) 22 | require_paths = [] 23 | parser = OptionParser.new do |o| 24 | o.on '-v', '--verbose', "log all tests" do |arg| 25 | Rgot.class_eval { @chatty = arg } 26 | end 27 | o.on '--version', "show Rgot version" do |arg| 28 | puts "rgot #{Rgot::VERSION} (ruby #{RUBY_VERSION})" 29 | exit 0 30 | end 31 | o.on '--bench [regexp]', "benchmark" do |arg| 32 | unless arg 33 | raise Rgot::OptionError, "missing argument for flag --bench" 34 | end 35 | opts.bench = arg 36 | end 37 | o.on '--benchtime [sec]', "benchmark running time" do |arg| 38 | opts.benchtime = arg 39 | end 40 | o.on '--timeout [sec]', "set timeout sec to testing" do |arg| 41 | opts.timeout = arg 42 | end 43 | o.on '--cpu [count,...]', "set cpu counts of comma split" do |arg| 44 | opts.cpu = arg 45 | end 46 | o.on '--thread [count,...]', "set thread counts of comma split" do |arg| 47 | opts.thread = arg 48 | end 49 | o.on '--require [path]', "load some code before running" do |arg| 50 | require_paths << arg 51 | end 52 | o.on '--load-path [path]', "Specify $LOAD_PATH directory" do |arg| 53 | $LOAD_PATH.unshift(arg) 54 | end 55 | o.on '--fuzz [regexp]', "run the fuzz test matching `regexp`" do |arg| 56 | unless arg 57 | raise Rgot::OptionError, "missing argument for flag --fuzz" 58 | end 59 | opts.fuzz = arg 60 | end 61 | o.on '--fuzztime [sec]', "time to spend fuzzing; default is to run indefinitely" do |arg| 62 | opts.fuzztime = arg 63 | end 64 | end 65 | parser.parse!(@argv) 66 | 67 | require_paths.each do |path| 68 | require path 69 | end 70 | end 71 | 72 | def testing_files 73 | if @argv.empty? 74 | Dir.glob("./**/*_test.rb") 75 | else 76 | @argv.flat_map do |target| 77 | if File.file?(target) 78 | File.expand_path(target) 79 | elsif File.directory?(target) 80 | Dir.glob("./#{target}/**/*_test.rb") 81 | else 82 | warn "#{target} is not file or directory" 83 | end 84 | end.compact 85 | end 86 | end 87 | 88 | def main_process(opts) 89 | code = 0 90 | 91 | testing_files.each do |testing_file| 92 | result = child_process(opts, testing_file) 93 | unless result == 0 94 | code = 1 95 | end 96 | end 97 | 98 | code 99 | end 100 | 101 | def child_process(opts, testing_file) 102 | node = RubyVM::AbstractSyntaxTree.parse_file(testing_file).children[2] 103 | test_module_name = find_toplevel_name(node) 104 | 105 | if opts.fuzz 106 | # fuzzing observes changes in coverage. 107 | require 'coverage' 108 | Coverage.start(oneshot_lines: true) 109 | end 110 | require testing_file 111 | 112 | test_module = Object.const_get(test_module_name) 113 | tests = [] 114 | benchmarks = [] 115 | examples = [] 116 | fuzz_targets = [] 117 | main = nil 118 | methods = test_module.public_instance_methods 119 | methods.grep(/\Atest_/).each do |m| 120 | if m == :test_main && main.nil? 121 | main = Rgot::InternalTest.new(test_module, m) 122 | else 123 | tests << Rgot::InternalTest.new(test_module, m) 124 | end 125 | end 126 | 127 | methods.grep(/\Abenchmark_/).each do |m| 128 | benchmarks << Rgot::InternalBenchmark.new(test_module, m) 129 | end 130 | 131 | methods.grep(/\Aexample_?/).each do |m| 132 | examples << Rgot::InternalExample.new(test_module, m) 133 | end 134 | 135 | methods.grep(/\Afuzz_/).each do |m| 136 | fuzz_targets << Rgot::InternalFuzzTarget.new(test_module, m) 137 | end 138 | 139 | m = Rgot::M.new( 140 | test_module: test_module, 141 | tests: tests, 142 | benchmarks: benchmarks, 143 | examples: examples, 144 | fuzz_targets: fuzz_targets, 145 | opts: opts 146 | ) 147 | if main 148 | main.module.extend main.module 149 | main.module.instance_method(:test_main).bind(main.module).call(m) 150 | else 151 | m.run 152 | end 153 | end 154 | 155 | def find_toplevel_name(node) 156 | case node.type 157 | when :MODULE 158 | find_toplevel_name(node.children.first) 159 | when :CONST, :COLON3 160 | node.children.first 161 | when :COLON2 162 | case node.children 163 | in [nil, sym] 164 | # module Foo 165 | sym 166 | in [namespace, sym] 167 | # module Foo::Bar 168 | find_toplevel_name(namespace) 169 | end 170 | when :BLOCK 171 | module_nodes = node.children.select do |c| 172 | c.type == :MODULE && find_toplevel_name(c).end_with?("Test") 173 | end 174 | raise "no module found" if module_nodes.empty? 175 | raise "*Test module should be one for each file" if 1 < module_nodes.length 176 | 177 | find_toplevel_name(module_nodes.first) 178 | else 179 | raise node.type.to_s 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/rgot/common.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'thread' 4 | require 'pathname' 5 | 6 | module Rgot 7 | class Common 8 | # @dynamic output, output= 9 | attr_accessor :output 10 | 11 | def initialize 12 | @output = "".dup 13 | @failed = false 14 | @skipped = false 15 | @finished = false 16 | @start = Rgot.now 17 | @mutex = Thread::Mutex.new 18 | end 19 | 20 | def failed? 21 | @mutex.synchronize { @failed } 22 | end 23 | 24 | def skipped? 25 | @mutex.synchronize { @skipped } 26 | end 27 | 28 | def finished? 29 | @mutex.synchronize { @finished } 30 | end 31 | 32 | def fail! 33 | @mutex.synchronize { @failed = true } 34 | end 35 | 36 | def skip! 37 | @mutex.synchronize { @skipped = true } 38 | end 39 | 40 | def finish! 41 | @mutex.synchronize { @finished = true } 42 | end 43 | 44 | def log(*args) 45 | internal_log(args.map(&:to_s).join(' ')) 46 | nil 47 | end 48 | 49 | def logf(*args) 50 | internal_log(sprintf(*args)) 51 | nil 52 | end 53 | 54 | def error(*args) 55 | internal_log(args.map(&:to_s).join(' ')) 56 | fail! 57 | nil 58 | end 59 | 60 | def errorf(*args) 61 | internal_log(sprintf(*args)) 62 | fail! 63 | nil 64 | end 65 | 66 | def fatal(*args) 67 | internal_log(args.map(&:to_s).join(' ')) 68 | fail_now 69 | end 70 | 71 | def fatalf(*args) 72 | internal_log(sprintf(*args)) 73 | fail_now 74 | end 75 | 76 | def skip(*args) 77 | internal_log(args.map(&:to_s).join(' ')) 78 | skip_now 79 | end 80 | 81 | def skipf(*args) 82 | internal_log(sprintf(*args)) 83 | skip_now 84 | end 85 | 86 | def skip_now 87 | skip! 88 | finish! 89 | throw :skip 90 | end 91 | 92 | def fail_now 93 | fail! 94 | finish! 95 | throw :skip 96 | end 97 | 98 | private 99 | 100 | def decorate(str) 101 | # internal_log -> synchronize -> internal_log -> other log -> running method 102 | c = caller[4] 103 | path = c.sub(/:.*/, '') 104 | line = c.match(/:(\d+?):/)&.[](1) 105 | relative_path = Pathname.new(path).relative_path_from(Pathname.new(Dir.pwd)).to_s 106 | # Every line is indented at least 4 spaces. 107 | " #{relative_path}:#{line}: #{str}\n" 108 | end 109 | 110 | def internal_log(msg) 111 | @mutex.synchronize { @output << decorate(msg) } 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/rgot/example_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ripper' 4 | 5 | module Rgot 6 | class ExampleParser < Ripper 7 | 8 | # @dynamic examples, examples= 9 | attr_accessor :examples 10 | 11 | def initialize(code) 12 | super 13 | @examples = [] 14 | @in_def = false 15 | @has_output = false 16 | @output = "".dup 17 | end 18 | 19 | def on_def(method, args, body) 20 | @examples << ExampleOutput.new(method.to_sym, @output.dup) 21 | @output.clear 22 | @has_output = false 23 | @in_def = false 24 | end 25 | 26 | def on_comment(a) 27 | if @in_def 28 | if @has_output 29 | @output << a.sub(/\A#\s*/, '') 30 | else 31 | if /#\s*Output:\s*(.*?\n)/ =~ a 32 | text = $1 33 | if 0 < text.length || text[0] != "\n" 34 | @output << text 35 | end 36 | @has_output = true 37 | end 38 | end 39 | end 40 | end 41 | 42 | def on_kw(a) 43 | case a 44 | when "def" 45 | @in_def = true 46 | when "end" 47 | @in_def = false 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rgot/f.rb: -------------------------------------------------------------------------------- 1 | module Rgot 2 | # def fuzz_foo(f) 3 | # f.add(5, "hello") 4 | # f.fuzz do |t, i, s| 5 | # ... 6 | # end 7 | class F < Common 8 | class Options 9 | # @dynamic fuzz, fuzz=, fuzztime, fuzztime= 10 | attr_accessor :fuzz 11 | attr_accessor :fuzztime 12 | def initialize(fuzz:, fuzztime:) 13 | @fuzz = fuzz 14 | @fuzztime = fuzztime 15 | end 16 | end 17 | 18 | class CorpusEntry 19 | # @dynamic values, values=, is_seed, is_seed=, path, path= 20 | attr_accessor :values 21 | attr_accessor :is_seed 22 | attr_accessor :path 23 | def initialize(values:, is_seed:, path:) 24 | @values = values 25 | @is_seed = is_seed 26 | @path = path 27 | end 28 | 29 | def mutate_values 30 | @values.map do |value| 31 | if generator = SUPPORTED_TYPES[value.class] 32 | generator.call(value) 33 | else 34 | raise "unsupported type #{value.class}" 35 | end 36 | end 37 | end 38 | end 39 | 40 | class Coordinator 41 | # @dynamic count, count=, interesting_count, interesting_count= 42 | attr_accessor :count 43 | attr_accessor :interesting_count 44 | 45 | def initialize(warmup_input_count:) 46 | @warmup_input_count = warmup_input_count 47 | @before_cov = 0 48 | @start_time = Rgot.now 49 | @count = 0 50 | @interesting_count = 0 51 | @count_last_log = 0 52 | @time_last_log = 0.0 53 | end 54 | 55 | def start_logger 56 | Thread.new do 57 | loop do 58 | log_stats 59 | sleep 3 60 | end 61 | end 62 | end 63 | 64 | def diff_coverage 65 | current_cov = Coverage.peek_result.sum do |path, hash| 66 | hash.map do |_, covs| 67 | covs.length 68 | end.sum 69 | end 70 | (current_cov - @before_cov).tap { @before_cov = current_cov } 71 | end 72 | 73 | def log_stats 74 | rate = Float(count - @count_last_log) / (Rgot.now - @time_last_log) 75 | total = @warmup_input_count + interesting_count 76 | printf "fuzz: elapsed: %ds, execs: %d (%d/sec), new interesting: %d (total: %d)\n", 77 | elapsed, count, rate, interesting_count, total 78 | 79 | duration = Rgot.now - @time_last_log 80 | @count_last_log = count 81 | @time_last_log = Rgot.now 82 | end 83 | 84 | private 85 | 86 | def elapsed 87 | (Rgot.now - @start_time).round 88 | end 89 | end 90 | 91 | SUPPORTED_TYPES = { 92 | TrueClass => ->(v) { [true, false].sample }, 93 | FalseClass => ->(v) { [true, false].sample }, 94 | Integer => ->(v) { Random.rand(v) }, 95 | Float => ->(v) { Random.rand(v) }, 96 | String => ->(v) { Random.bytes(v.length) }, 97 | } 98 | 99 | # @dynamic name 100 | attr_reader :name 101 | 102 | def initialize(fuzz_target:, opts:) 103 | super() 104 | @opts = opts 105 | @fuzz_target = fuzz_target 106 | @fuzz_block = nil 107 | @module = fuzz_target.module 108 | @name = fuzz_target.name 109 | @corpus = [] 110 | end 111 | 112 | # TODO: DRY with T 113 | def run 114 | catch(:skip) { call } 115 | finish! 116 | rescue => e 117 | fail! 118 | raise e 119 | end 120 | 121 | def run_testing 122 | run 123 | report if !fuzz? || failed? 124 | end 125 | 126 | def run_fuzzing 127 | return unless fuzz? 128 | raise("must call after #fuzz") unless @fuzz_block 129 | 130 | coordinator = Coordinator.new( 131 | warmup_input_count: @corpus.length 132 | ) 133 | coordinator.start_logger 134 | 135 | t = T.new(@fuzz_target.module, @fuzz_target.name) 136 | 137 | begin 138 | Timeout.timeout(@opts.fuzztime.to_f) do 139 | loop do 140 | @corpus.each do |entry| 141 | values = entry.mutate_values 142 | 143 | @fuzz_block.call(t, *values) 144 | 145 | if 0 < coordinator.diff_coverage 146 | coordinator.interesting_count += 1 147 | end 148 | coordinator.count += 1 149 | fail! if t.failed? 150 | end 151 | end 152 | end 153 | rescue Timeout::Error, Interrupt 154 | coordinator.log_stats 155 | end 156 | 157 | report 158 | end 159 | 160 | def add(*args) 161 | args.each do |arg| 162 | unless SUPPORTED_TYPES.key?(arg.class) 163 | raise "unsupported type to Add #{arg.class}" 164 | end 165 | end 166 | entry = CorpusEntry.new( 167 | values: args.dup, 168 | is_seed: true, 169 | path: "seed##{@corpus.length}" 170 | ) 171 | @corpus.push(entry) 172 | end 173 | 174 | def fuzz(&block) 175 | unless block 176 | raise LocalJumpError, "must set block" 177 | end 178 | unless 2 <= block.arity 179 | raise "fuzz target must receive at least two arguments" 180 | end 181 | 182 | t = T.new(@fuzz_target.module, @fuzz_target.name) 183 | 184 | @corpus.each do |entry| 185 | unless entry.values.length == (block.arity - 1) 186 | raise "wrong number of values in corpus entry: #{entry.values.length}, want #{block.arity - 1}" 187 | end 188 | block.call(t, *entry.values.dup) 189 | fail! if t.failed? 190 | end 191 | 192 | @fuzz_block = block 193 | 194 | nil 195 | end 196 | 197 | def fuzz? 198 | return false unless @opts.fuzz 199 | return false unless Regexp.new(@opts.fuzz.to_s).match?(@fuzz_target.name) 200 | true 201 | end 202 | 203 | def report 204 | puts @output if Rgot.verbose? && !@output.empty? 205 | duration = Rgot.now - @start 206 | template = "--- \e[%sm%s\e[m: %s (%.2fs)\n" 207 | if failed? 208 | printf template, [41, 1].join(';'), "FAIL", @name, duration 209 | elsif Rgot.verbose? 210 | if skipped? 211 | printf template, [44, 1].join(';'), "SKIP", @name, duration 212 | else 213 | printf template, [42, 1].join(';'), "PASS", @name, duration 214 | end 215 | end 216 | end 217 | 218 | private 219 | 220 | def call 221 | test_method = @module.instance_method(@name).bind(@module) 222 | if test_method.arity == 0 223 | path, line = test_method.source_location 224 | warn "#{path}:#{line} `#{test_method.name}' is not running. It's a testing method name, But not have argument" 225 | else 226 | test_method.call(self) 227 | end 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /lib/rgot/m.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stringio' 4 | require 'etc' 5 | require 'timeout' 6 | 7 | module Rgot 8 | class M 9 | class Options < Struct.new( 10 | :bench, 11 | :benchtime, 12 | :timeout, 13 | :cpu, 14 | :thread, 15 | :fuzz, 16 | :fuzztime, 17 | ); end 18 | 19 | def initialize(tests:, benchmarks:, examples:, fuzz_targets: nil, test_module: nil, opts: Options.new) 20 | unless fuzz_targets 21 | raise "Require `fuzz_targets` keyword" if Gem::Version.new("2.0") <= Gem::Version.new(Rgot::VERSION) 22 | warn "`Rgot::M#initialize` will require the `fuzz_targets` keyword in the next major version." 23 | end 24 | unless test_module 25 | raise "Require `test_module` keyword" if Gem::Version.new("2.0") <= Gem::Version.new(Rgot::VERSION) 26 | warn "`Rgot::M#initialize` will require the `test_module` keyword in the next major version." 27 | end 28 | 29 | @tests = tests 30 | @benchmarks = benchmarks 31 | @examples = examples 32 | @fuzz_targets = fuzz_targets || [] 33 | @test_module = test_module 34 | @opts = opts 35 | 36 | @cpu_list = [] 37 | @thread_list = [] 38 | @fs = @fuzz_targets.map do |fuzz_target| 39 | F.new( 40 | fuzz_target: fuzz_target, 41 | opts: F::Options.new( 42 | fuzz: opts.fuzz, 43 | fuzztime: opts.fuzztime, 44 | ) 45 | ) 46 | end 47 | end 48 | 49 | def run 50 | duration = Rgot.now 51 | test_ok = false 52 | fuzz_targets_ok = false 53 | example_ok = false 54 | 55 | if @tests.empty? && @benchmarks.empty? && @examples.empty? && @fuzz_targets.empty? 56 | warn "rgot: warning: no tests to run" 57 | end 58 | 59 | begin 60 | parse_option 61 | rescue Rgot::OptionError 62 | puts sprintf("%s\t%s\t%.3fs", "FAIL", @test_module, Rgot.now - duration) 63 | raise 64 | end 65 | 66 | Timeout.timeout(@opts.timeout.to_f) do 67 | test_ok = run_tests 68 | fuzz_targets_ok = run_fuzz_tests 69 | example_ok = run_examples 70 | end 71 | 72 | if !test_ok || !example_ok || !fuzz_targets_ok 73 | puts "FAIL" 74 | puts "exit status 1" 75 | puts sprintf("%s\t%s\t%.3fs", "FAIL", @test_module, Rgot.now - duration) 76 | return 1 77 | end 78 | 79 | if !run_fuzzing() 80 | puts "FAIL" 81 | puts "exit status 1" 82 | puts sprintf("%s\t%s\t%.3fs", "FAIL", @test_module, Rgot.now - duration) 83 | return 1 84 | end 85 | 86 | puts "PASS" 87 | run_benchmarks 88 | puts sprintf("%s\t%s\t%.3fs", "ok ", @test_module, Rgot.now - duration) 89 | 90 | 0 91 | end 92 | 93 | private 94 | 95 | def parse_option 96 | cpu = @opts.cpu || (Etc.respond_to?(:nprocessors) ? Etc.nprocessors : '1').to_s 97 | @cpu_list = cpu.split(',').map { |i| 98 | j = i.to_i 99 | raise Rgot::OptionError, "invalid value #{i.inspect} for --cpu" unless 0 < j 100 | j 101 | } 102 | 103 | @thread_list = (@opts.thread || "1").split(',').map { |i| 104 | j = i.to_i 105 | raise Rgot::OptionError, "invalid value #{i.inspect} for --thread" unless 0 < j 106 | j 107 | } 108 | end 109 | 110 | def run_tests 111 | ok = true 112 | @tests.each do |test| 113 | t = T.new(test.module, test.name.to_sym) 114 | if Rgot.verbose? 115 | puts "=== RUN #{test.name}" 116 | end 117 | t.run 118 | ok = ok && !t.failed? 119 | end 120 | ok 121 | end 122 | 123 | def run_benchmarks 124 | ok = true 125 | return ok unless @opts.bench 126 | @benchmarks.each do |bench| 127 | next unless /#{@opts.bench}/ =~ bench.name 128 | 129 | @cpu_list.each do |procs| 130 | @thread_list.each do |threads| 131 | opts = B::Options.new 132 | opts.benchtime = @opts.benchtime 133 | opts.procs = procs 134 | opts.threads = threads 135 | b = B.new(bench.module, bench.name.to_sym, opts) 136 | 137 | benchname = bench.name.to_s 138 | benchname << "-#{procs}" if 1 < procs 139 | benchname << "(#{threads})" if 1 < threads 140 | print "#{benchname}\t" 141 | result = b.run 142 | if b.failed? 143 | ok = false 144 | next 145 | end 146 | puts result 147 | if 0 < b.output.length 148 | printf("--- BENCH: %s\n%s", benchname, b.output) 149 | end 150 | end 151 | end 152 | end 153 | ok 154 | end 155 | 156 | def run_fuzz_tests 157 | ok = true 158 | @fs.each do |f| 159 | if Rgot.verbose? 160 | if f.fuzz? 161 | puts "=== FUZZ #{f.name}" 162 | else 163 | puts "=== RUN #{f.name}" 164 | end 165 | end 166 | f.run_testing 167 | ok = ok && !f.failed? 168 | end 169 | ok 170 | end 171 | 172 | def run_fuzzing 173 | if @fuzz_targets.empty? || @opts.fuzz.nil? 174 | return true 175 | end 176 | 177 | fuzzing_fs = @fs.select(&:fuzz?) 178 | 179 | if fuzzing_fs.empty? 180 | puts "rgot: warning: no fuzz tests to fuzz" 181 | return true 182 | end 183 | 184 | if fuzzing_fs.length > 1 185 | names = fuzzing_fs.map(&:name) 186 | puts "rgot: will not fuzz, --fuzz matches more than one fuzz test: #{names.inspect}" 187 | return false 188 | end 189 | 190 | ok = true 191 | 192 | fuzzing_fs.each do |f| 193 | f.run_fuzzing 194 | ok = ok && !f.failed? 195 | end 196 | 197 | ok 198 | end 199 | 200 | def run_examples 201 | ok = true 202 | @examples.each do |example| 203 | if Rgot.verbose? 204 | puts "=== RUN #{example.name}" 205 | end 206 | 207 | start = Rgot.now 208 | example.module.extend(example.module) 209 | method = example.module.instance_method(example.name).bind(example.module) 210 | out, _ = capture do 211 | method.call 212 | end 213 | file = method.source_location&.[](0) or raise("bug") 214 | example_parser = ExampleParser.new(File.read(file)) 215 | example_parser.parse 216 | e = example_parser.examples.find { |er| er.name == example.name } or raise("bug") 217 | 218 | duration = Rgot.now - start 219 | if e.output.strip != out.strip 220 | printf("--- FAIL: %s (%.2fs)\n", e.name, duration) 221 | ok = false 222 | puts "got:" 223 | puts out.strip 224 | puts "want:" 225 | puts e.output.strip 226 | elsif Rgot.verbose? 227 | printf("--- PASS: %s (%.2fs)\n", e.name, duration) 228 | end 229 | end 230 | ok 231 | end 232 | 233 | def capture 234 | raise LocalJumpError, "no block given" unless block_given? 235 | 236 | orig_out, orig_err = $stdout, $stderr 237 | out, err = StringIO.new, StringIO.new 238 | $stdout, $stderr = out, err 239 | yield 240 | [out.string, err.string] 241 | ensure 242 | $stdout, $stderr = orig_out, orig_err 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /lib/rgot/pb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rgot 4 | class PB 5 | def initialize(bn:) 6 | @bn = bn 7 | end 8 | 9 | def next 10 | (0 < @bn).tap { @bn -= 1 } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/rgot/t.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rgot 4 | class T < Common 5 | def initialize(test_module, name) 6 | super() 7 | @module = test_module 8 | @name = name 9 | @module.extend @module 10 | end 11 | 12 | def run 13 | catch(:skip) { call } 14 | finish! 15 | report 16 | rescue => e 17 | fail! 18 | report 19 | raise e 20 | end 21 | 22 | def report 23 | puts @output if Rgot.verbose? && !@output.empty? 24 | duration = Rgot.now - @start 25 | template = "--- \e[%sm%s\e[m: %s (%.2fs)\n" 26 | if failed? 27 | printf template, [41, 1].join(';'), "FAIL", @name, duration 28 | elsif Rgot.verbose? 29 | if skipped? 30 | printf template, [44, 1].join(';'), "SKIP", @name, duration 31 | else 32 | printf template, [42, 1].join(';'), "PASS", @name, duration 33 | end 34 | end 35 | end 36 | 37 | def call 38 | test_method = @module.instance_method(@name).bind(@module) 39 | if test_method.arity == 0 40 | path, line = test_method.source_location 41 | warn "#{path}:#{line} `#{test_method.name}' is not running. It's a testing method name, But not have argument" 42 | else 43 | test_method.call(self) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/rgot/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rgot 4 | VERSION = "1.5.0" 5 | end 6 | -------------------------------------------------------------------------------- /rbs_collection.lock.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | sources: 3 | - name: ruby/gem_rbs_collection 4 | remote: https://github.com/ruby/gem_rbs_collection.git 5 | revision: main 6 | repo_dir: gems 7 | path: ".gem_rbs_collection" 8 | gems: 9 | - name: pathname 10 | version: '0' 11 | source: 12 | type: stdlib 13 | - name: optparse 14 | version: '0' 15 | source: 16 | type: stdlib 17 | - name: timeout 18 | version: '0' 19 | source: 20 | type: stdlib 21 | - name: etc 22 | version: '0' 23 | source: 24 | type: stdlib 25 | - name: coverage 26 | version: '0' 27 | source: 28 | type: stdlib 29 | - name: concurrent-ruby 30 | version: '1.1' 31 | source: 32 | type: git 33 | name: ruby/gem_rbs_collection 34 | revision: 9576ce5b109170f1ba8a42671bfafb64ab95bd23 35 | remote: https://github.com/ruby/gem_rbs_collection.git 36 | repo_dir: gems 37 | - name: io-console 38 | version: '0' 39 | source: 40 | type: stdlib 41 | - name: logger 42 | version: '0' 43 | source: 44 | type: stdlib 45 | - name: monitor 46 | version: '0' 47 | source: 48 | type: stdlib 49 | gemfile_lock_path: Gemfile.lock 50 | -------------------------------------------------------------------------------- /rbs_collection.yaml: -------------------------------------------------------------------------------- 1 | # Download sources 2 | sources: 3 | - name: ruby/gem_rbs_collection 4 | remote: https://github.com/ruby/gem_rbs_collection.git 5 | revision: main 6 | repo_dir: gems 7 | 8 | # A directory to install the downloaded RBSs 9 | path: .gem_rbs_collection 10 | 11 | gems: 12 | - name: pathname 13 | - name: optparse 14 | - name: timeout 15 | - name: etc 16 | - name: coverage 17 | 18 | # Skip loading rbs gem's RBS. 19 | # It's unnecessary if you don't use rbs as a library. 20 | - name: rbs 21 | ignore: true 22 | - name: steep 23 | ignore: true 24 | - name: activesupport 25 | ignore: true 26 | - name: ast 27 | ignore: true 28 | - name: csv 29 | ignore: true 30 | - name: i18n 31 | ignore: true 32 | - name: json 33 | ignore: true 34 | - name: listen 35 | ignore: true 36 | - name: fileutils 37 | ignore: true 38 | - name: minitest 39 | ignore: true 40 | - name: parallel 41 | ignore: true 42 | - name: rainbow 43 | ignore: true 44 | - name: rgot 45 | ignore: true 46 | - name: securerandom 47 | ignore: true 48 | - name: strscan 49 | ignore: true 50 | - name: forwardable 51 | ignore: true 52 | - name: mutex_m 53 | ignore: true 54 | -------------------------------------------------------------------------------- /rgot.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rgot/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "rgot" 8 | spec.version = Rgot::VERSION 9 | spec.authors = ["ksss"] 10 | spec.email = ["co000ri@gmail.com"] 11 | 12 | spec.summary = %q{Ruby + Golang Testing = Rgot} 13 | spec.description = %q{Rgot is a testing package convert from golang testing} 14 | spec.homepage = "https://github.com/ksss/rgot" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|go)/}) } 18 | spec.bindir = "bin" 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_development_dependency "bundler" 23 | spec.add_development_dependency "rake" 24 | end 25 | -------------------------------------------------------------------------------- /sig/patch.rbs: -------------------------------------------------------------------------------- 1 | # patch 2 | class Ripper 3 | end 4 | -------------------------------------------------------------------------------- /sig/rgot.rbs: -------------------------------------------------------------------------------- 1 | module Rgot 2 | VERSION: "1.2.0" 3 | 4 | def self.now: () -> Float 5 | def self.benchmark: (?Hash[Symbol, String] opts_hash) { (B) -> void } -> BenchmarkResult 6 | def self.verbose?: () -> bool 7 | 8 | class Cli 9 | @argv: Array[String] 10 | 11 | def initialize: (untyped argv) -> void 12 | def run: () -> void 13 | 14 | private 15 | 16 | def parse_option: (Rgot::M::Options opts) -> void 17 | def main_process: (Rgot::M::Options opts) -> void 18 | def testing_files: () -> Array[String] 19 | def child_process: (Rgot::M::Options opts, String testing_file) -> Integer 20 | # `node` is RubyVM::AbstractSyntaxTree::Node 21 | def find_toplevel_name: (untyped node) -> Symbol 22 | end 23 | 24 | class Common 25 | @failed: bool 26 | @skipped: bool 27 | @finished: bool 28 | @start: Float 29 | @mutex: Thread::Mutex 30 | 31 | attr_accessor output: String 32 | 33 | def initialize: () -> void 34 | 35 | def failed?: () -> bool 36 | def skipped?: () -> bool 37 | def finished?: () -> bool 38 | def fail!: () -> void 39 | def skip!: () -> void 40 | def finish!: () -> void 41 | def log: (*untyped) -> nil 42 | def logf: (*untyped) -> nil 43 | def error: (*untyped) -> nil 44 | def errorf: (*untyped) -> nil 45 | def fatal: (*untyped) -> bot 46 | def fatalf: (*untyped) -> bot 47 | def skip: (*untyped) -> bot 48 | def skipf: (*untyped) -> bot 49 | def skip_now: () -> bot 50 | def fail_now: () -> bot 51 | 52 | private 53 | 54 | def decorate: (String) -> String 55 | def internal_log: (String msg) -> void 56 | end 57 | 58 | class M 59 | class Options 60 | attr_accessor bench: String? 61 | attr_accessor benchtime: String? 62 | attr_accessor timeout: String? 63 | attr_accessor cpu: String? 64 | attr_accessor thread: String? 65 | attr_accessor fuzz: String? 66 | attr_accessor fuzztime: String? 67 | end 68 | 69 | @tests: Array[InternalTest] 70 | @benchmarks: Array[InternalBenchmark] 71 | @examples: Array[InternalExample] 72 | @fuzz_targets: Array[InternalFuzzTarget] 73 | @fs: Array[F] 74 | @test_module: Module? 75 | @opts: M::Options 76 | @cpu_list: Array[Integer] 77 | @thread_list: Array[Integer] 78 | 79 | def initialize: ( 80 | tests: Array[InternalTest], 81 | benchmarks: Array[InternalBenchmark], 82 | examples: Array[InternalExample], 83 | fuzz_targets: Array[InternalFuzzTarget], 84 | ?test_module: Module?, 85 | ?opts: Options 86 | ) -> void 87 | def run: () -> Integer 88 | 89 | private 90 | 91 | def run_tests: () -> bool 92 | def run_benchmarks: () -> bool 93 | def run_fuzz_tests: () -> bool 94 | def run_fuzzing: () -> bool 95 | def run_examples: () -> bool 96 | end 97 | 98 | class T < Common 99 | @module: Module 100 | @name: Symbol 101 | 102 | def initialize: (Module test_module, Symbol name) -> void 103 | def run: () -> void 104 | def report: () -> void 105 | def call: () -> void 106 | end 107 | 108 | class B < Common 109 | class Options 110 | attr_accessor procs: Integer 111 | attr_accessor threads: Integer 112 | attr_accessor benchtime: String? 113 | end 114 | 115 | @module: Module? 116 | @name: Symbol? 117 | @opts: B::Options 118 | @timer_on: bool 119 | @duration: Float 120 | 121 | attr_accessor n: Integer 122 | 123 | def initialize: (Module? benchmark_module, Symbol? name, ?B::Options opts) -> void 124 | def start_timer: () -> void 125 | def stop_timer: () -> void 126 | def reset_timer: () -> void 127 | def run: () ?{ (B) -> void } -> BenchmarkResult 128 | def run_parallel: () { (PB) -> void } -> void 129 | 130 | private 131 | 132 | def run_n: (Integer n, ?Proc? block) -> void 133 | end 134 | 135 | class F < Common 136 | class Options 137 | attr_accessor fuzz: String? 138 | attr_accessor fuzztime: String? 139 | def initialize: (fuzz: String?, fuzztime: String?) -> void 140 | end 141 | class CorpusEntry 142 | attr_accessor values: Array[untyped] 143 | attr_accessor is_seed: bool 144 | attr_accessor path: String 145 | def initialize: (values: Array[untyped], is_seed: bool, path: String) -> void 146 | def mutate_values: () -> Array[untyped] 147 | end 148 | class Coordinator 149 | attr_accessor count: Integer 150 | attr_accessor interesting_count: Integer 151 | def initialize: (warmup_input_count: Integer) -> void 152 | @warmup_input_count: Integer 153 | @before_cov: Integer 154 | @start_time: Float 155 | @count: Integer 156 | @interesting_count: Integer 157 | @count_last_log: Integer 158 | @time_last_log: Float 159 | def start_logger: () -> void 160 | def diff_coverage: () -> Integer 161 | def log_stats: () -> void 162 | private 163 | def elapsed: () -> Integer 164 | end 165 | SUPPORTED_TYPES: Hash[untyped, ^(untyped) -> untyped] 166 | @fuzz_target: InternalFuzzTarget 167 | @fuzz_block: Proc? 168 | @opts: F::Options 169 | @corpus: Array[CorpusEntry] 170 | @module: Module 171 | attr_reader name: Symbol 172 | def initialize: (fuzz_target: InternalFuzzTarget, opts: F::Options) -> void 173 | def run: () -> void 174 | def run_testing: () -> void 175 | def run_fuzzing: () -> void 176 | def add: (*untyped) -> void 177 | def fuzz: () { (*untyped) -> void } -> void 178 | def fuzz?: () -> bool 179 | def report: () -> void 180 | 181 | private 182 | 183 | def call: () -> void 184 | end 185 | 186 | class PB 187 | @bn: Integer 188 | def initialize: (bn: Integer) -> void 189 | def next: () -> bool 190 | end 191 | 192 | class BenchmarkResult 193 | attr_reader n: Integer # int // The number of iterations. 194 | attr_reader t: Float # time.Duration // The total time taken. 195 | def initialize: (n: Integer, t: Float) -> void 196 | end 197 | 198 | class ExampleParser < Ripper 199 | @in_def: bool 200 | @has_output: bool 201 | @output: String 202 | attr_accessor examples: Array[ExampleOutput] 203 | end 204 | 205 | class OptionError < StandardError 206 | end 207 | 208 | class InternalTest 209 | attr_accessor module: Module 210 | attr_accessor name: Symbol 211 | end 212 | 213 | class InternalBenchmark 214 | attr_accessor module: Module 215 | attr_accessor name: Symbol 216 | end 217 | 218 | class InternalExample 219 | attr_accessor module: Module 220 | attr_accessor name: Symbol 221 | end 222 | 223 | class InternalFuzzTarget 224 | attr_accessor module: Module 225 | attr_accessor name: Symbol 226 | end 227 | 228 | class ExampleOutput 229 | attr_accessor output: String 230 | attr_accessor name: Symbol 231 | end 232 | end 233 | -------------------------------------------------------------------------------- /test/bar/bar_test.rb: -------------------------------------------------------------------------------- 1 | module BarTest 2 | def test_bar(t) 3 | t.log "test_bar is ok" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/bar/baz/baz_test.rb: -------------------------------------------------------------------------------- 1 | module BazTest 2 | def test_baz(t) 3 | t.error("test_baz is ng") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/benchmark_test.rb: -------------------------------------------------------------------------------- 1 | require_relative './sample' 2 | 3 | module BenchmarkTest 4 | def fibo(n) 5 | if n < 2 6 | n 7 | else 8 | fibo(n - 1) + fibo(n - 2) 9 | end 10 | end 11 | 12 | def benchmark_sum(b) 13 | i = 0 14 | s = Sample.new 15 | while i < b.n 16 | s.sum(10, 2) 17 | i += 1 18 | end 19 | end 20 | 21 | def benchmark_parallel(b) 22 | b.run_parallel do |pb| 23 | while pb.next 24 | fibo(25) 25 | end 26 | end 27 | end 28 | 29 | def benchmark_skip(b) 30 | b.skip "skip!" 31 | raise "never reach!" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/example_fail_test.rb: -------------------------------------------------------------------------------- 1 | # Output: 2 | # bad output 3 | module ExamplePassTest 4 | # Output: 5 | # bad output 6 | def example_singleline 7 | puts "ok go" 8 | # Output: ng back 9 | end 10 | 11 | def example_multiline 12 | puts "Hello" 13 | puts "I'm example" 14 | # Output: 15 | # bye 16 | # I'm fail 17 | end 18 | # Output: 19 | # bad output 20 | end 21 | # Output: 22 | # bad output 23 | -------------------------------------------------------------------------------- /test/example_pass_test.rb: -------------------------------------------------------------------------------- 1 | # Output: 2 | # bad output 3 | module ExamplePassTest 4 | # Output: 5 | # bad output 6 | def example_singleline 7 | puts "ok go" 8 | # Output: ok go 9 | end 10 | 11 | def example_multiline 12 | # multiline example testing start 13 | puts "Hello" 14 | puts "I'm example" 15 | puts "Example is so cool" 16 | # Output: 17 | # Hello 18 | # I'm example 19 | # Example is so cool 20 | end 21 | # Output: 22 | # bad output 23 | end 24 | # Output: 25 | # bad output 26 | -------------------------------------------------------------------------------- /test/fail_test.rb: -------------------------------------------------------------------------------- 1 | require_relative './sample' 2 | 3 | module FailTest 4 | def test_fail(t) 5 | s = Sample.new 6 | sum = s.sum(2.0, 3) 7 | unless sum.kind_of?(Integer) 8 | t.error("expect Integer got #{sum.class}") 9 | end 10 | unless sum == 5 11 | t.error("expect 5 got #{sum}") 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fatal_test.rb: -------------------------------------------------------------------------------- 1 | require_relative './sample' 2 | 3 | module FatalTest 4 | def test_fatal(t) 5 | s = Sample.new 6 | sum = s.sum(nil, nil) 7 | unless sum.kind_of?(Integer) 8 | t.error("expect Integer got #{sum.class}") 9 | end 10 | unless sum == 5 11 | t.error("expect 5 got #{sum}") 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/foo/foo_test.rb: -------------------------------------------------------------------------------- 1 | module FooTest 2 | def test_foo(t) 3 | t.log("test_foo is ok") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fuzzing_fail_test.rb: -------------------------------------------------------------------------------- 1 | module FuzzingFailTest 2 | def fuzz_fail(f) 3 | f.add(5, "hello") 4 | f.fuzz do |t, i, s| 5 | t.error("fail in fuzz") 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/fuzzing_multi_test.rb: -------------------------------------------------------------------------------- 1 | module FuzzingMultiTest 2 | def fuzz_multi1(f) 3 | f.add(5, "hello") 4 | f.fuzz do |t, i, s| 5 | end 6 | end 7 | 8 | def fuzz_multi2(f) 9 | f.add(5, "hello") 10 | f.fuzz do |t, i, s| 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/fuzzing_pass_test.rb: -------------------------------------------------------------------------------- 1 | module FuzzingPassTest 2 | def fuzz_pass(f) 3 | unless f.kind_of?(Rgot::F) 4 | f.error("unexpected type #{f.class} - #{f}") 5 | end 6 | f.add(100, "hello") 7 | f.fuzz do |t, i, s| 8 | unless t.kind_of?(Rgot::T) 9 | t.error("unexpected type #{t.class} - #{t}") 10 | end 11 | unless i.kind_of?(Integer) 12 | t.error("unexpected type #{i.class} - #{i}") 13 | end 14 | unless s.kind_of?(String) 15 | t.error("unexpected type #{s.class} - #{s}") 16 | end 17 | # loop 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/main_test.rb: -------------------------------------------------------------------------------- 1 | module MainTest 2 | def test_main(m) 3 | puts "start in main" 4 | code = m.run 5 | puts "end in main" 6 | code 7 | end 8 | 9 | def test_some_1(t) 10 | puts "run testing" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/multi_module_fail_test.rb: -------------------------------------------------------------------------------- 1 | module FooTest 2 | def test_foo(t) 3 | raise "should not call" 4 | end 5 | end 6 | 7 | module BarTest 8 | def test_bar(t) 9 | raise "should not call" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/multi_module_pass_test.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module MonkeyPatch 4 | def test_call(t) 5 | raise("should not call") 6 | end 7 | end 8 | 9 | module MultiModuleTest 10 | def test_call(t) 11 | puts "multi module test ok" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/pass_test.rb: -------------------------------------------------------------------------------- 1 | require_relative './sample' 2 | 3 | module PassTest 4 | class TypeSum < Struct.new(:left, :right, :expect) 5 | end 6 | 7 | DATA = [ 8 | TypeSum.new(2, 3, 5), 9 | TypeSum.new(12, 9, 21), 10 | TypeSum.new(85, 42, 127), 11 | ] 12 | 13 | def test_pass(t) 14 | s = Sample.new 15 | DATA.each do |ts| 16 | sum = s.sum(ts.left, ts.right) 17 | unless sum.kind_of?(Integer) 18 | t.error("expect Integer got #{sum.class}") 19 | end 20 | unless sum == ts.expect 21 | t.error("expect #{ts.expect} got #{sum}") 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/rgot_benchmark_test.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module RgotBenchmarkTest 4 | def test_benchmark(t) 5 | cmd = "rgot test/benchmark_test.rb --benchtime 0.4 --bench sum --cpu=1" 6 | out = `#{cmd}` 7 | if /benchmark_sum\s+\d+\s+\d+\s+ns\/op/ !~ out 8 | t.error("expect output benchmark report. got #{out}") 9 | end 10 | end 11 | 12 | def test_benchmark_parallel_procs(t) 13 | cmd = "rgot test/benchmark_test.rb --benchtime 0.1 --bench parallel --cpu=2,4" 14 | out = `#{cmd}` 15 | expect_out = <<-OUT.chomp 16 | benchmark_parallel-2\\s+\\d+\\s+\\d+\\s+ns/op 17 | benchmark_parallel-4\\s+\\d+\\s+\\d+\\s+ns/op 18 | ok\\s+BenchmarkTest\\s+\\d+\\.\\d+s 19 | OUT 20 | if /#{expect_out}/m !~ out 21 | t.error("expect match out. got #{out}") 22 | end 23 | end 24 | 25 | def test_benchmark_skip(t) 26 | cmd = "rgot test/benchmark_test.rb --bench skip --cpu=1" 27 | out = `#{cmd}` 28 | expect_out = <<-'OUT' 29 | PASS 30 | benchmark_skip\s+\d\s+\d\s+ns/op 31 | ---\s+BENCH:\s+benchmark_skip 32 | \s+.*?:\s+skip! 33 | ok\s+BenchmarkTest\s+\d.\d+s 34 | OUT 35 | if /#{expect_out}/ !~ out 36 | t.errorf("expect output not match want:%s got:%s", expect_out, out) 37 | end 38 | end 39 | 40 | def test_benchmark_concurrent_threads(t) 41 | cmd = "rgot test/benchmark_test.rb --benchtime 0.01 --bench parallel --cpu=2,4 --thread=2,4" 42 | out = `#{cmd}` 43 | expect_out = <<-OUT.chomp 44 | benchmark_parallel-2\\(2\\)\\s+\\d+\\s+\\d+\\s+ns/op 45 | benchmark_parallel-2\\(4\\)\\s+\\d+\\s+\\d+\\s+ns/op 46 | benchmark_parallel-4\\(2\\)\\s+\\d+\\s+\\d+\\s+ns/op 47 | benchmark_parallel-4\\(4\\)\\s+\\d+\\s+\\d+\\s+ns/op 48 | ok\\s+BenchmarkTest\\s+\\d+.\\d+s 49 | OUT 50 | if /#{expect_out}/m !~ out 51 | t.error("expect match out. got #{out}") 52 | end 53 | end 54 | 55 | def test_benchmark_invalid_option(t) 56 | cmd = "rgot test/benchmark_test.rb --cpu=2,-1" 57 | out, err, status = Open3.capture3(cmd) 58 | if status.success? 59 | t.error("expect process not success `#{cmd}` got #{status}") 60 | end 61 | if /invalid value "-1" for --cpu \(Rgot::OptionError\)/ !~ err 62 | error_class = err.match(/\((.*?)\)/)[1] 63 | t.log(err) 64 | t.error("expect Rgot::OptionError got #{error_class}") 65 | end 66 | if /FAIL\s.*/ !~ out 67 | t.error("expect FAIL `#{cmd}` got \"#{out}\"") 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/rgot_common_test.rb: -------------------------------------------------------------------------------- 1 | module RgotCommonTest 2 | def test_common_log(t) 3 | c = Rgot::Common.new 4 | ret, line = c.log(1, 2, 3), __LINE__ 5 | if ret != nil 6 | t.error("expect return nil got #{ret}") 7 | end 8 | if /test\/rgot_common_test.rb:#{line}.*?1 2 3/ !~ c.output 9 | t.error("expect output '1 2 3' got #{c.output.inspect}") 10 | end 11 | end 12 | 13 | def test_common_logf(t) 14 | c = Rgot::Common.new 15 | ret, line = c.logf("%d-%d-%d", 1, 2, 3), __LINE__ 16 | if ret != nil 17 | t.error("expect return nil got #{ret}") 18 | end 19 | if /test\/rgot_common_test.rb:#{line}.*?1-2-3/ !~ c.output 20 | t.error("expect output '1-2-3' got #{c.output.inspect}") 21 | end 22 | end 23 | 24 | def test_common_error(t) 25 | c = Rgot::Common.new 26 | ret, line = c.error(1, 2, 3), __LINE__ 27 | if ret != nil 28 | t.error("expect return nil got #{ret}") 29 | end 30 | if /test\/rgot_common_test.rb:#{line}.*?1 2 3/ !~ c.output 31 | t.error("expect output '1 2 3' got #{c.output.inspect}") 32 | end 33 | unless c.failed? 34 | t.error("expect status 'failed' but not") 35 | end 36 | end 37 | 38 | def test_common_errorf(t) 39 | c = Rgot::Common.new 40 | ret, line = c.errorf("%d-%d-%d", 1, 2, 3), __LINE__ 41 | if ret != nil 42 | t.error("expect return nil got #{ret}") 43 | end 44 | if /test\/rgot_common_test.rb:#{line}.*?1-2-3/ !~ c.output 45 | t.error("expect output '1-2-3' got #{c.output.inspect}") 46 | end 47 | unless c.failed? 48 | t.error("expect status 'failed' but not") 49 | end 50 | end 51 | 52 | def test_common_fatal(t) 53 | c = Rgot::Common.new 54 | ret = line = nil 55 | catch(:skip) do 56 | line = __LINE__ 57 | ret = c.fatal(1, 2, 3) 58 | raise "never reach" 59 | end 60 | if ret != nil 61 | t.error("expect return nil got #{ret}") 62 | end 63 | if /test\/rgot_common_test.rb:#{line + 1}.*?1 2 3/ !~ c.output 64 | t.error("expect output '1 2 3' got #{c.output.inspect}") 65 | end 66 | unless c.failed? && c.finished? 67 | t.error("expect status 'failed' and 'finished' but not") 68 | end 69 | end 70 | 71 | def test_common_fatalf(t) 72 | c = Rgot::Common.new 73 | ret = line = nil 74 | catch(:skip) do 75 | line = __LINE__ 76 | ret = c.fatalf("%d-%d-%d", 1, 2, 3) 77 | raise "never reach" 78 | end 79 | if ret != nil 80 | t.error("expect return nil got #{ret}") 81 | end 82 | if /test\/rgot_common_test.rb:#{line + 1}.*?1-2-3/ !~ c.output 83 | t.error("expect output '1-2-3' got #{c.output.inspect}") 84 | end 85 | unless c.failed? && c.finished? 86 | t.error("expect status 'failed' and 'finished' but not") 87 | end 88 | end 89 | 90 | def test_common_skip(t) 91 | c = Rgot::Common.new 92 | ret = line = nil 93 | catch(:skip) do 94 | line = __LINE__ 95 | ret = c.skip(1, 2, 3) 96 | raise "never reach" 97 | end 98 | if ret != nil 99 | t.error("expect return nil got #{ret}") 100 | end 101 | if /test\/rgot_common_test.rb:#{line + 1}.*?1 2 3/ !~ c.output 102 | t.error("expect output '1 2 3' got #{c.output.inspect}") 103 | end 104 | unless c.skipped? && c.finished? 105 | t.error("expect status 'skip' and 'finished' but not") 106 | end 107 | end 108 | 109 | def test_common_skipf(t) 110 | c = Rgot::Common.new 111 | ret = line = nil 112 | catch(:skip) do 113 | line = __LINE__ 114 | ret = c.skipf("%d-%d-%d", 1, 2, 3) 115 | raise "never reach" 116 | end 117 | if ret != nil 118 | t.error("expect return nil got #{ret}") 119 | end 120 | if /test\/rgot_common_test.rb:#{line + 1}.*?1-2-3/ !~ c.output 121 | t.error("expect output '1-2-3' got #{c.output.inspect}") 122 | end 123 | unless c.skipped? && c.finished? 124 | t.error("expect status 'skip' and 'finished' but not") 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/rgot_example_test.rb: -------------------------------------------------------------------------------- 1 | module RgotExampleTest 2 | def test_example_pass(t) 3 | cmd = "rgot test/example_pass_test.rb" 4 | out = `#{cmd}`.chomp 5 | if /ok\s+ExamplePassTest/ !~ out 6 | t.error("want PASS got '#{out}'") 7 | end 8 | end 9 | 10 | def test_example_fail(t) 11 | cmd = "rgot test/example_fail_test.rb" 12 | out = `#{cmd}` 13 | expect_out = <<-OUT.chomp 14 | --- FAIL: example_singleline (\d+.\d+s) 15 | got: 16 | ok go 17 | want: 18 | ng back 19 | --- FAIL: example_multiline (\d+.\d+s) 20 | got: 21 | Hello 22 | I'm example 23 | want: 24 | bye 25 | I'm fail 26 | FAIL 27 | FAIL ExamplePassTest 28 | OUT 29 | if Regexp.new(expect_out) =~ out 30 | t.error("\n--- expect:\n#{expect_out}\n--- got:\n#{out}\n") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/rgot_fuzzing_test.rb: -------------------------------------------------------------------------------- 1 | module RgotFuzzingTest 2 | def test_fuzzing_pass(t) 3 | cmd = "rgot test/fuzzing_pass_test.rb --fuzz . --fuzztime 0.1" 4 | out = `#{cmd}` 5 | 6 | expect_out = <<-OUT.chomp 7 | \\Afuzz: elapsed: \\d+s, execs: \\d+ \\(\\d+/sec\\), new interesting: \\d+ \\(total: \\d+\\) 8 | fuzz: elapsed: \\d+s, execs: \\d+ \\(\\d+/sec\\), new interesting: \\d+ \\(total: \\d+\\) 9 | PASS 10 | ok\\s+FuzzingPassTest\\s+\\d+\\.\\d{3}s 11 | OUT 12 | unless /#{expect_out}/m.match?(out) 13 | t.error("expect output fuzz report. got #{out}") 14 | end 15 | end 16 | 17 | def test_fuzzing_pass_verbose(t) 18 | cmd = "rgot test/fuzzing_pass_test.rb -v --fuzz . --fuzztime 0.1" 19 | out = `#{cmd}` 20 | 21 | expect_out = <<-OUT.chomp 22 | \\A===\\s+FUZZ\\s+fuzz_pass 23 | fuzz: elapsed: \\d+s, execs: \\d+ \\(\\d+/sec\\), new interesting: \\d+ \\(total: \\d+\\) 24 | fuzz: elapsed: \\d+s, execs: \\d+ \\(\\d+/sec\\), new interesting: \\d+ \\(total: \\d+\\) 25 | ---\\s+.*PASS.*:\\s+fuzz_pass\\s+\\(\\d+\\.\\d{2}s\\) 26 | PASS 27 | ok\\s+FuzzingPassTest\\s+\\d+\\.\\d{3}s 28 | OUT 29 | unless /#{expect_out}/m.match?(out) 30 | t.error("expect output fuzz report. got #{out}") 31 | end 32 | end 33 | 34 | def test_fuzzing_fail(t) 35 | cmd = "rgot test/fuzzing_fail_test.rb --fuzz . --fuzztime 0.1" 36 | out = `#{cmd}` 37 | 38 | expect_out = <<-OUT.chomp 39 | \\A---\\s+.*FAIL.*:\\s+fuzz_fail\\s+\\(\\d+.\\d{2}s\\) 40 | FAIL 41 | exit\\sstatus\\s1 42 | FAIL\\s+FuzzingFailTest\\s+\\d+\.\\d{3}s 43 | OUT 44 | unless /#{expect_out}/m.match?(out) 45 | t.error("expect output fuzz report. got #{out}") 46 | end 47 | end 48 | 49 | def test_fuzzing_fail_verbose(t) 50 | cmd = "rgot test/fuzzing_fail_test.rb -v --fuzz . --fuzztime 0.1" 51 | out = `#{cmd}` 52 | 53 | expect_out = <<-OUT.chomp 54 | \\A===\\s+FUZZ\\s+fuzz_fail 55 | ---\\s+.*FAIL.*:\\s+fuzz_fail\\s+\\(\\d+.\\d{2}s\\) 56 | FAIL 57 | exit\\sstatus\\s1 58 | FAIL\\s+FuzzingFailTest\\s+\\d+\.\\d{3}s 59 | OUT 60 | unless /#{expect_out}/m.match?(out) 61 | t.error("expect output fuzz report. got #{out}") 62 | end 63 | end 64 | 65 | def test_fuzzing_multi(t) 66 | cmd = "rgot test/fuzzing_multi_test.rb -v --fuzz . --fuzztime 0.1" 67 | out = `#{cmd}` 68 | 69 | expect_out = <<-OUT.chomp 70 | rgot: will not fuzz, --fuzz matches more than one fuzz test: \\[:fuzz_multi\\d, :fuzz_multi\\d\\] 71 | FAIL 72 | exit status 1 73 | FAIL\\s+FuzzingMultiTest\\s+\\d+\.\\d{3}s 74 | OUT 75 | unless /#{expect_out}/m.match?(out) 76 | t.error("expect output fuzz report. got #{out}") 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/rgot_test.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module RgotTest 4 | # This method should be running before testing 5 | def test_main(m) 6 | m.run 7 | end 8 | 9 | def test_pass(t) 10 | cmd = "rgot test/pass_test.rb -v" 11 | out = `#{cmd}` 12 | unless /PASS/ =~ out 13 | t.error("expect PASS `#{cmd}` got #{out}") 14 | end 15 | unless $?.success? 16 | t.error("expect exit status 0, but got #{$?}") 17 | end 18 | end 19 | 20 | def test_fail(t) 21 | cmd = "rgot test/fail_test.rb -v" 22 | out = `#{cmd}` 23 | unless /FAIL/ =~ out 24 | t.error("expect FAIL `#{cmd}` got #{out}") 25 | end 26 | unless !$?.success? 27 | t.error("expect exit status fail, but not") 28 | end 29 | end 30 | 31 | def test_fatal(t) 32 | cmd = "rgot test/fatal_test.rb -v" 33 | out, err, status = Open3.capture3(cmd) 34 | if status.success? 35 | t.error("expect process not success `#{cmd}` got #{status}") 36 | end 37 | if /\(NoMethodError\)/ !~ err 38 | error_class = err.match(/\((.*?)\)/)[1] 39 | t.log(err) 40 | t.error("expect NoMethodError got #{error_class}") 41 | end 42 | unless /FAIL/ =~ out 43 | t.error("expect FAIL `#{cmd}` got #{out}") 44 | end 45 | end 46 | 47 | def test_skip(t) 48 | cmd = "rgot test/skip_test.rb -v" 49 | out = `#{cmd}` 50 | unless /SKIP/ =~ out 51 | t.error("expect skip `#{cmd}` got #{out}") 52 | end 53 | end 54 | 55 | def test_timeout(t) 56 | cmd = "rgot test/timeout_test.rb -v --timeout 0.1" 57 | out, err, status = Open3.capture3(cmd) 58 | if status.success? 59 | t.error("expect process not success `#{cmd}` got #{status}") 60 | end 61 | if /\(Timeout::Error\)/ !~ err 62 | error_class = err.match(/\((.*?)\)/)[1] 63 | t.log(err) 64 | t.error("expect Timeout::Error got #{error_class}") 65 | end 66 | if /^ok\s+\d/ =~ out 67 | t.error("expect not print 'ok' got #{out}") 68 | end 69 | end 70 | 71 | def test_main_method(t) 72 | cmd = "rgot test/main_test.rb -v" 73 | out = `#{cmd}` 74 | if /start in main.*?run testing.*?end in main/m !~ out 75 | t.error("expect output start -> run -> end got '#{out}'") 76 | end 77 | end 78 | 79 | def test_multi_module_pass(t) 80 | cmd = "rgot test/multi_module_pass_test.rb -v" 81 | out = `#{cmd}` 82 | if /multi module test ok/ !~ out 83 | t.error("expect output 'multi module test ok', but got '#{out}'") 84 | end 85 | end 86 | 87 | def test_multi_module_fail(t) 88 | cmd = "rgot test/multi_module_fail_test.rb -v" 89 | out, err, status = Open3.capture3(cmd) 90 | unless !status.success? 91 | t.error("should be fail, but success") 92 | end 93 | unless err.include?("*Test module should be one for each file") 94 | t.error("expect output '*Test module should be one for each file', but got '#{err}'") 95 | end 96 | end 97 | 98 | def test_main_method_return_code(t) 99 | code = Rgot::M.new(tests: [], benchmarks: [], examples: [], fuzz_targets: [], test_module: RgotTest).run 100 | unless Integer === code 101 | t.error("Rgot::M#run return expect to exit code, got #{code}") 102 | end 103 | end 104 | 105 | def test_single_dir(t) 106 | cmd = "rgot -v test/foo" 107 | out = `#{cmd}` 108 | if /test_foo is ok.*?PASS/m !~ out 109 | t.error("want PASS got '#{out}'") 110 | end 111 | end 112 | 113 | def test_multi_dir_and_module(t) 114 | cmd = "rgot -v test/bar test/bar/baz" 115 | out = `#{cmd}` 116 | if /test_bar is ok.*?PASS.*?test_baz is ng.*?FAIL/m !~ out 117 | t.error("want PASS and FAIL massage got '#{out}'") 118 | end 119 | end 120 | 121 | def test_rgot_benchmark(t) 122 | start = Time.now 123 | last_n = 0 124 | result = Rgot.benchmark(benchtime: 0.2) { |b| 125 | unless Rgot::B === b 126 | t.error("expect instance of Rgot::B got #{b.class}") 127 | end 128 | unless 0 < b.n 129 | t.error("b.n expect over 0 since loop times") 130 | end 131 | sleep(b.n * 0.01) 132 | last_n = b.n 133 | } 134 | duration = (Time.now - start) 135 | unless 0.2 < duration 136 | t.error("too fast benchmark running time expect=0.2s got=#{duration}s") 137 | end 138 | unless duration < 5 139 | t.error("too slow benchmark running time expect=5s got=#{duration}s") 140 | end 141 | unless 5 <= last_n 142 | t.error("too few times last_n=#{last_n}") 143 | end 144 | unless Rgot::BenchmarkResult === result 145 | t.error("expect instance of Rgot::BenchmarkResult got #{result.class}") 146 | end 147 | end 148 | 149 | def test_rgot_parallel_benchmark(t) 150 | r, w = IO.pipe 151 | Rgot.benchmark do |b| 152 | b.run_parallel do |pb| 153 | r.close 154 | w.write("t") 155 | unless pb.kind_of?(Rgot::PB) 156 | t.error("run_parallel block argument expect instance of Rgot::PB, got #{pb}") 157 | end 158 | unless ok = pb.next 159 | t.error("first Rgot::PB#next expect `true' got #{ok}") 160 | end 161 | end 162 | end 163 | w.close 164 | ret, err = go { r.read_nonblock(1) } 165 | if ret != "t" || err 166 | t.error("expect call block in run_parallel") 167 | end 168 | end 169 | 170 | def test_rgot_help(t) 171 | out = `rgot -h` 172 | expect_out = <<-HELP 173 | Usage: rgot [options] 174 | -v, --verbose log all tests 175 | --version show Rgot version 176 | --bench [regexp] benchmark 177 | --benchtime [sec] benchmark running time 178 | --timeout [sec] set timeout sec to testing 179 | --cpu [count,...] set cpu counts of comma split 180 | --thread [count,...] set thread counts of comma split 181 | --require [path] load some code before running 182 | --load-path [path] Specify $LOAD_PATH directory 183 | --fuzz [regexp] run the fuzz test matching `regexp` 184 | --fuzztime [sec] time to spend fuzzing; default is to run indefinitely 185 | HELP 186 | if out != expect_out 187 | t.error("rgot -h README.md and test should be update") 188 | end 189 | end 190 | 191 | private 192 | 193 | def go 194 | ret = nil 195 | err = nil 196 | begin 197 | ret = yield 198 | rescue => e 199 | err = e 200 | end 201 | [ret, err] 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /test/sample.rb: -------------------------------------------------------------------------------- 1 | class Sample 2 | def sum(i, j) 3 | i + j 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/skip_test.rb: -------------------------------------------------------------------------------- 1 | require_relative './sample' 2 | 3 | module SkipTest 4 | def test_skip(t) 5 | t.skip "skip testing" 6 | raise "expect to unreach" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/timeout_test.rb: -------------------------------------------------------------------------------- 1 | require_relative './sample' 2 | 3 | module TimeoutTest 4 | def test_timeout(t) 5 | sleep 1 6 | end 7 | end 8 | --------------------------------------------------------------------------------