├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .vscode └── launch.json ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── exe └── flgen ├── flgen.gemspec ├── lib ├── flgen.rb └── flgen │ ├── arguments.rb │ ├── cli.rb │ ├── context.rb │ ├── exceptions.rb │ ├── file_list.rb │ ├── file_list_formatter.rb │ ├── file_list_xsim_formatter.rb │ ├── formatter.rb │ ├── predefined_macros.yaml │ ├── source_file.rb │ ├── version.rb │ └── vivado_tcl_formatter.rb ├── sample ├── bar │ ├── bar.list.rb │ ├── bar.sv │ ├── bar_lib │ │ └── .gitkeep │ └── baz │ │ ├── baz.list.rb │ │ └── baz.sv ├── foo.list.rb ├── foo.sv └── foo_lib.sv └── spec ├── flgen ├── cli_spec.rb ├── context_spec.rb ├── file_list_spec.rb └── source_file_spec.rb └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "bundler" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | ignore: 17 | - dependency-name: "*" 18 | update-types: [ "version-update:semver-patch" ] 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rspec: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | ruby: ['3.4', '3.3', '3.2', '3.1'] 12 | env: 13 | BUNDLE_WITH: 'development_common development_test' 14 | BUNDLE_WITHOUT: 'development_lint development_local' 15 | 16 | steps: 17 | - name: Checkout the repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby }} 24 | bundler-cache: true 25 | 26 | - name: Run RSpec 27 | run: | 28 | bundle exec rake coverage 29 | 30 | - name: Upload coverage report 31 | uses: codecov/codecov-action@v5 32 | with: 33 | name: ${{ matrix.ruby }} 34 | files: ./coverage/coverage.xml 35 | fail_ci_if_error: false 36 | env: 37 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | Gemfile.lock 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_todo.yml 3 | 4 | AllCops: 5 | TargetRubyVersion: 3.1 6 | Exclude: 7 | - spec/**/* 8 | - sample/**/* 9 | NewCops: enable 10 | SuggestExtensions: false 11 | 12 | Layout/EndOfLine: 13 | EnforcedStyle: lf 14 | 15 | Layout/LineEndStringConcatenationIndentation: 16 | Enabled: false 17 | 18 | Layout/LineLength: 19 | Max: 90 20 | 21 | Layout/MultilineMethodCallIndentation: 22 | EnforcedStyle: indented_relative_to_receiver 23 | 24 | Lint/AmbiguousOperatorPrecedence: 25 | Enabled: false 26 | 27 | Metrics/AbcSize: 28 | AllowedMethods: 29 | - option_parser 30 | 31 | Metrics/BlockLength: 32 | AllowedMethods: 33 | - OptionParser.new 34 | 35 | Metrics/ClassLength: 36 | Max: 150 37 | 38 | Metrics/MethodLength: 39 | Max: 15 40 | AllowedMethods: 41 | - option_parser 42 | 43 | Metrics/ModuleLength: 44 | Max: 130 45 | 46 | Naming/VariableNumber: 47 | EnforcedStyle: snake_case 48 | 49 | Style/AccessorGrouping: 50 | EnforcedStyle: separated 51 | 52 | Style/Alias: 53 | EnforcedStyle: prefer_alias_method 54 | 55 | Style/EmptyMethod: 56 | EnforcedStyle: expanded 57 | 58 | Style/RaiseArgs: 59 | EnforcedStyle: compact 60 | 61 | Style/SymbolArray: 62 | EnforcedStyle: brackets 63 | 64 | Style/WordArray: 65 | EnforcedStyle: brackets 66 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | Style/Documentation: 2 | Enabled: false 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "rdbg", 6 | "name": "Debug current RSpec spec file with rdbg", 7 | "request": "launch", 8 | "command": "rspec", 9 | "script": "${file}", 10 | "args": [], 11 | "useBundler": true, 12 | "askParameters": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in file_list_generator.gemspec 6 | gemspec 7 | 8 | group :development_common do 9 | gem 'bundler', require: false 10 | gem 'rake', require: false 11 | end 12 | 13 | group :development_test do 14 | gem 'rspec', '~> 3.13.0', require: false 15 | gem 'simplecov', '~> 0.22.0', require: false 16 | gem 'simplecov-cobertura', '~> 2.1.0', require: false 17 | end 18 | 19 | group :development_lint do 20 | gem 'rubocop', '~> 1.75.1', require: false 21 | end 22 | 23 | group :development_local do 24 | gem 'bump', '~> 0.10.0', require: false 25 | gem 'debug', require: false 26 | gem 'ruby-lsp', require: false 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/flgen.svg)](https://badge.fury.io/rb/flgen) 2 | [![CI](https://github.com/pezy-computing/flgen/actions/workflows/ci.yml/badge.svg)](https://github.com/pezy-computing/flgen/actions/workflows/ci.yml) 3 | [![codecov](https://codecov.io/github/pezy-computing/flgen/branch/master/graph/badge.svg?token=P5JSMPRV3J)](https://codecov.io/github/pezy-computing/flgen) 4 | 5 | # FLGen 6 | 7 | FLGen provides a DSL to write filelists and generator tool to generate a filelist which is given to EDA tools. 8 | 9 | ## Install 10 | 11 | ### Ruby 12 | 13 | FLGen is written in [Ruby](https://www.ruby-lang.org) programing language and its required version is 3.0 or later. You need to install Ruby before using FLGen. See [this page](https://www.ruby-lang.org/en/documentation/installation/) for further details. 14 | 15 | ### Install FLGen 16 | 17 | Use the command below to isntall FLGen. 18 | 19 | ``` 20 | $ gem install flgen 21 | ``` 22 | 23 | ## Filelist 24 | 25 | FLGen prives APIs listed below to describe your filelists. 26 | 27 | * `source_file(path, from: nil)` 28 | * Add the given source file to the current filelist. 29 | * `file_list(path, from: nil)` 30 | * Load the given filelist. 31 | * `library_file(path, from: nil)` 32 | * Add the given file to the list of library files. 33 | * `include_directory(path, from: nil)` 34 | * Add the given directory to the list of include direcotries. 35 | * `library_directory(path, from: nil)` 36 | * Add the given directory to the list of library directories. 37 | * `find_files(patterns, from: nil)` 38 | * Return an array of filenames matching the given patterns strings. 39 | * See [here](https://docs.ruby-lang.org/en/3.2/Dir.html#method-c-glob) for the format of a pattern string. 40 | * `find_file(patterns, from: nil)` 41 | * Return the first filename matching the given pattern strings. 42 | * `file?(path, from: :current)` 43 | * Return `true` if the given file exists. 44 | * `directory?(path, from: :current)` 45 | * Return `true` if the given directory exists. 46 | * `define_macro(name, value = nil)` 47 | * Define a text macro. 48 | * `undefine_macro(name)` 49 | * Undefine the given macro. 50 | * `macro?(name)`/`macro_defined?(name)` 51 | * Return `true` if the given macro is defined. 52 | * `macro(name)` 53 | * Return the value of the given macro. 54 | * `env?(name)` 55 | * Return `true` if the givne environment variable is defined. 56 | * `env(name)` 57 | * Retunr the value of the given environment variable. 58 | * `compile_argument(argument, tool: nil)` 59 | * Add the given argument to the list of compile arguments. 60 | * If `tool` is specified the given argument is added only when `tool` is matched with the targe tool. 61 | * `runtime_argumetn(argument, tool: nil)` 62 | * Add the given argument to the list of runtime arguments. 63 | * If `tool` is specified the given argument is added only when `tool` is matched with the targe tool. 64 | * `target_tool?(tool)` 65 | * Return `true` if the given tool is matched with the targe tool. 66 | * `default_search_path(**search_paths)` 67 | * Change the default behavior when the `from` argument is not specified. 68 | * `reset_default_search_path(*target_types)` 69 | * Reset the default behavior when the `from` argument is not specified. 70 | 71 | FLGen's filelist is designed as an inernal DSL with Ruby. Therefore you can use Ruby's syntax. For example: 72 | 73 | ```ruby 74 | if macro? :GATE_SIM 75 | source_file 'foo_top.v.gz' # synthsized netlist 76 | else 77 | source_file 'foo_top.sv' # RTL 78 | end 79 | ``` 80 | 81 | ### About the `from` argument 82 | 83 | The `from` argument is to specify how to search the given file or directory. You can specify one of three below. 84 | 85 | * a directory path 86 | * Search the given file or directory from the directory path specified by the `from` argument. 87 | * `:cwd` 88 | * Search the given file or directory from the current working directory. 89 | * `:current` 90 | * Search the given file or directory from the directory where the current filelist is. 91 | * `:root` 92 | * Search the given file or directory from the repository root directories where the `.git` directory is. 93 | * Serch order is descending order. 94 | * from upper root direcotries to local root direcoty 95 | * `:local_root` 96 | * Search the given file or directory from the repository root directory where the current filelist belongs to. 97 | 98 | Default behaviors when the `from` argument is not spcified are listed below: 99 | 100 | 101 | | API name | Default `from` argument | 102 | |:------------------|:------------------------| 103 | | source_file | :current | 104 | | file_list | :root | 105 | | library_file | :current | 106 | | include_directory | :current | 107 | | library_directory | :current | 108 | | find_files | :current | 109 | | find_file | :current | 110 | 111 | You can change the above default behaviors by using the `default_search_path` API. 112 | In addition, you can reset the default behaviors by using the `reset_default_search_path` API. 113 | 114 | ```ruby 115 | default_search_path source_file: :root, file_list: :current 116 | source_file 'foo.sv' # FLGen will search the 'foo.sv' file from the root directories. 117 | file_list 'bar.list.rb' # FLGen will eaarch the 'bar.list.rb' file from the directory where this file list is. 118 | 119 | reset_default_search_path :source_file, :file_list 120 | source_file 'baz.sv' # FLGen will search the 'baz.sv' file from the directory where this file list is. 121 | file_list 'qux.list.rb' # FLGen will eaarch the 'qux.list.rb' file from the root directories. 122 | ``` 123 | 124 | #### Example 125 | 126 | This is an exmaple directory structure. 127 | 128 | ``` 129 | foo_project 130 | +-- .git 131 | +-- bar_project 132 | | +-- .git 133 | | +-- common 134 | | | `-- common.sv 135 | | `-- src 136 | | + bar.list.rb 137 | | ` bar.sv 138 | `-- common 139 | `-- common.sv 140 | ``` 141 | 142 | * `source_file 'bar.sv', from: :current` @ `bar.list.rb` 143 | * `foo_project/bar_project/bar.sv` is added. 144 | * `source_file 'common/common.sv', from: :root` @ `bar.list.rb` 145 | * `foo_project/common/common.sv` is added 146 | * `source_file 'common/bar_common.sv', from: :local_root` @ `bar.list.rb` 147 | * `foo_project/bar_project/common/common.sv` is added 148 | 149 | ## Generator command 150 | 151 | `flgen` is the generator command and generate a filelist which is given to EDA tools from the given filelists. Command line options are listed below. 152 | 153 | * `--define-macro=MACRO[,MACRO]` 154 | * Define the given macros 155 | * `--include-directory=DIR[,DIR]` 156 | * Specify include directories 157 | * `--compile` 158 | * If this option is specified the generated filelist contains source file path, arguments to define macros, arguments to specify include directories and arguments specified by `compile_argument` API. 159 | * `--runtime` 160 | * If this option is specified the generated filelist contains arguments specified by `runtime_argumetn` 161 | * `--tool=TOOL` 162 | * Specify the target tool. 163 | * `--rm-ext=EXT[,EXT]` 164 | * Remove specifyed file extentions from source file path. 165 | * `--collect-ext=EXT[,EXT]` 166 | * The generated filelist contains source file pash which has the specified file extentions. 167 | * `--format=FORMAT` 168 | * Specify the format of the generated filelist. 169 | * If no format is specified FLGen will generate a generated filelist for major EDA tools. 170 | * If `vivado-tcl` is specified FLGen will generate a TCL script to load source files for Vivado synthesis. 171 | * If `filelist-xsim` is specified FLGen will generate a filelist for Vivado simulator. 172 | * `--output=FILE` 173 | * Specify the path of the generated filelist 174 | * The generated fileslist is output to STDOUT if no path is specified. 175 | * `--[no-]print-header` 176 | * Specify whether or not the output filelist includes its file header or not. 177 | * `--source-file-only` 178 | * The generated filelist contains source file path only if this option is specified. 179 | 180 | ## Example 181 | 182 | You can find an exmpale from [here](https://github.com/pezy-computing/flgen/tree/master/sample). 183 | 184 | ``` 185 | $ flgen --output=filelist.f sample/foo.list.rb 186 | $ cat filelist.f 187 | // flgen version 0.17.0 188 | // applied arguments 189 | // --output=filelist.f 190 | // sample/foo.list.rb 191 | +define+BAR_0 192 | +define+BAR_1=1 193 | +incdir+/home/taichi/workspace/pezy/flgen/sample/bar 194 | +incdir+/home/taichi/workspace/pezy/flgen/sample/bar/baz 195 | -y /home/taichi/workspace/pezy/flgen/sample/bar/bar_lib 196 | -v /home/taichi/workspace/pezy/flgen/sample/foo_lib.sv 197 | -foo_0 198 | /home/taichi/workspace/pezy/flgen/sample/foo.sv 199 | /home/taichi/workspace/pezy/flgen/sample/bar/bar.sv 200 | /home/taichi/workspace/pezy/flgen/sample/bar/baz/baz.sv 201 | ``` 202 | 203 | [rggen-sample-testbench](https://github.com/rggen/rggen-sample-testbench) uses FLGen. This can be a practical example. 204 | 205 | * https://github.com/rggen/rggen-sample-testbench/blob/master/env/compile.rb 206 | * https://github.com/rggen/rggen-sample-testbench/blob/master/rtl/compile.rb 207 | 208 | ## License 209 | 210 | FLGen is licensed under the Apache-2.0 license. See [LICNESE](LICENSE) and below for further details. 211 | 212 | ``` 213 | Copyright 2022 PEZY Computing K.K. 214 | 215 | Licensed under the Apache License, Version 2.0 (the "License"); 216 | you may not use this file except in compliance with the License. 217 | You may obtain a copy of the License at 218 | 219 | http://www.apache.org/licenses/LICENSE-2.0 220 | 221 | Unless required by applicable law or agreed to in writing, software 222 | distributed under the License is distributed on an "AS IS" BASIS, 223 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 224 | See the License for the specific language governing permissions and 225 | limitations under the License. 226 | ``` 227 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'bundler/gem_tasks' 5 | require 'rspec/core/rake_task' 6 | 7 | CLEAN << 'coverage' 8 | 9 | RSpec::Core::RakeTask.new(:spec) 10 | 11 | unless ENV.key?('CI') 12 | require 'bump/tasks' 13 | require 'rubocop/rake_task' 14 | RuboCop::RakeTask.new(:rubocop) 15 | end 16 | 17 | desc 'Run all RSpec code exmaples and collect code coverage' 18 | task :coverage do 19 | ENV['COVERAGE'] = 'yes' 20 | Rake::Task['spec'].execute 21 | end 22 | 23 | task default: :spec 24 | -------------------------------------------------------------------------------- /exe/flgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) 5 | require 'flgen' 6 | 7 | begin 8 | FLGen::CLI.new.run(ARGV) 9 | rescue FLGen::FLGenError => e 10 | abort e.to_s 11 | end 12 | -------------------------------------------------------------------------------- /flgen.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('lib/flgen/version', __dir__) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'flgen' 7 | spec.version = FLGen::VERSION 8 | spec.authors = ['Taichi Ishitani'] 9 | spec.email = ['ishitani@pezy.co.jp'] 10 | 11 | spec.summary = 'Filelist generator' 12 | spec.description = spec.summary 13 | spec.homepage = 'https://github.com/pezy-computing/flgen' 14 | spec.license = 'Apache-2.0' 15 | 16 | spec.metadata = { 17 | 'bug_tracker_uri' => 'https://github.com/pezy-computing/flgen/issues', 18 | 'rubygems_mfa_required' => 'true', 19 | 'source_code_uri' => 'https://github.com/pezy-computing/flgen', 20 | 'wiki_uri' => 'https://github.com/pezy-computing/flgen/wiki' 21 | } 22 | 23 | spec.files = `git ls-files exe lib sample README.md LICENSE`.split($RS) 24 | spec.bindir = 'exe' 25 | spec.executables = `git ls-files -- exe/*`.split($RS).map(&File.method(:basename)) 26 | spec.require_paths = ['lib'] 27 | spec.required_ruby_version = '>= 3.1' 28 | end 29 | -------------------------------------------------------------------------------- /lib/flgen.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'digest/md5' 4 | require 'optparse' 5 | require 'pathname' 6 | require 'yaml' 7 | require_relative 'flgen/version' 8 | require_relative 'flgen/exceptions' 9 | require_relative 'flgen/source_file' 10 | require_relative 'flgen/arguments' 11 | require_relative 'flgen/file_list' 12 | require_relative 'flgen/context' 13 | require_relative 'flgen/formatter' 14 | require_relative 'flgen/file_list_formatter' 15 | require_relative 'flgen/vivado_tcl_formatter' 16 | require_relative 'flgen/file_list_xsim_formatter' 17 | require_relative 'flgen/cli' 18 | -------------------------------------------------------------------------------- /lib/flgen/arguments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | module Arguments 5 | class Base 6 | def initialize(type, tool) 7 | @type = type 8 | @tool = tool 9 | end 10 | 11 | attr_reader :type 12 | attr_reader :tool 13 | 14 | def match_tool?(target_tool) 15 | tool.nil? || target_tool && (tool == target_tool) || false 16 | end 17 | end 18 | 19 | class Define < Base 20 | def initialize(name, value) 21 | super(:define, nil) 22 | @name = name 23 | @value = value 24 | end 25 | 26 | attr_reader :name 27 | attr_reader :value 28 | end 29 | 30 | class LibraryFile < Base 31 | def initialize(path) 32 | super(:library_file, nil) 33 | @path = path 34 | end 35 | 36 | attr_reader :path 37 | end 38 | 39 | class Include < Base 40 | def initialize(path) 41 | super(:include, nil) 42 | @path = path 43 | end 44 | 45 | attr_reader :path 46 | end 47 | 48 | class LibraryDirectory < Base 49 | def initialize(path) 50 | super(:library_directory, nil) 51 | @path = path 52 | end 53 | 54 | attr_reader :path 55 | end 56 | 57 | class Generic < Base 58 | def initialize(argument, tool) 59 | super(:generic, tool) 60 | @argument = argument 61 | end 62 | 63 | attr_reader :argument 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/flgen/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class CLI 5 | PROGRAM_NAME = 'flgen' 6 | 7 | def run(args) 8 | options, remain_args = parse_options(args.dup) 9 | context = parse_file_list(options, remain_args) 10 | output_result(args, context) 11 | end 12 | 13 | private 14 | 15 | def parse_options(args) 16 | options = { 17 | macros: [], include_directories: [], runtime: false, tool: nil, 18 | rm_ext: [], collect_ext: [], format: :filelist, print_header: true, 19 | source_file_only: false 20 | } 21 | option_parser(options).parse!(args) 22 | [options, args] 23 | end 24 | 25 | def option_parser(options) 26 | OptionParser.new do |parser| 27 | parser.version = VERSION 28 | parser.program_name = PROGRAM_NAME 29 | 30 | parser.on('--define-macro=MACRO[,MACRO]', Array) do |macros| 31 | options[:macros].concat(macros) 32 | end 33 | parser.on('--include-directory=DIR[,DIR]', Array) do |directories| 34 | options[:include_directories].concat(directories) 35 | end 36 | parser.on('--compile') do 37 | options[:runtime] = false 38 | end 39 | parser.on('--runtime') do 40 | options[:runtime] = true 41 | end 42 | parser.on('--tool=TOOL') do |tool| 43 | options[:tool] = tool.to_sym 44 | end 45 | parser.on('--rm-ext=EXT[,EXT]', Array) do |ext| 46 | options[:rm_ext].concat(ext) 47 | end 48 | parser.on('--collect-ext=EXT[,EXT]', Array) do |ext| 49 | options[:collect_ext].concat(ext) 50 | end 51 | parser.on('--format=FORMAT') do |format| 52 | options[:format] = format.to_sym 53 | end 54 | parser.on('--output=FILE') do |file| 55 | options[:output] = file 56 | end 57 | parser.on('--[no-]print-header') do |value| 58 | options[:print_header] = value 59 | end 60 | parser.on('--source-file-only') do |value| 61 | options[:source_file_only] = value 62 | end 63 | end 64 | end 65 | 66 | def parse_file_list(options, args) 67 | context = create_context(options) 68 | top_level = FileList.new(context, '') 69 | args.each { |arg| load_file_list(top_level, arg) } 70 | context 71 | end 72 | 73 | def create_context(options) 74 | context = Context.new(options) 75 | context 76 | .options[:macros] 77 | .each(&context.method(:define_macro)) 78 | context 79 | .options[:include_directories] 80 | .each(&context.method(:add_include_directory)) 81 | context 82 | end 83 | 84 | def load_file_list(top_level, arg) 85 | path = File.expand_path(arg) 86 | top_level.file_list(path) 87 | end 88 | 89 | def output_result(args, context) 90 | formatter = create_formatter(context) 91 | print_header(context, formatter, args) 92 | if context.options[:output] 93 | File.open(context.options[:output], 'w') { |io| formatter.output(io) } 94 | else 95 | formatter.output($stdout) 96 | end 97 | end 98 | 99 | def create_formatter(context) 100 | formatter = Formatter.formatters[context.options[:format]] 101 | formatter.new(context) 102 | end 103 | 104 | def print_header(_context, formatter, args) 105 | formatter.header_lines << "#{PROGRAM_NAME} version #{FLGen::VERSION}" 106 | formatter.header_lines << 'applied arguments' 107 | args.each do |arg| 108 | formatter.header_lines << " #{arg}" 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/flgen/context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class Context 5 | def initialize(options) 6 | @options = options 7 | define_predefined_macros 8 | end 9 | 10 | attr_reader :options 11 | 12 | def source_files 13 | @source_files ||= [] 14 | end 15 | 16 | def add_source_file(path) 17 | return if runtime? 18 | 19 | file = SourceFile.new(path) 20 | add_source_file?(file) && 21 | (source_files << file.remove_ext(@options[:rm_ext])) 22 | end 23 | 24 | def add_library_file(path) 25 | return if runtime? 26 | 27 | file = SourceFile.new(path) 28 | add_library_file?(file) && 29 | add_compile_argument(Arguments::LibraryFile.new(file)) 30 | end 31 | 32 | def define_macro(macro, value = nil) 33 | k, v = 34 | if value.nil? && macro.respond_to?(:split) 35 | macro.split('=', 2) 36 | else 37 | [macro, value] 38 | end 39 | add_macro_definition(k, v, false) 40 | end 41 | 42 | def undefine_macro(macro) 43 | remove_macro(macro) 44 | end 45 | 46 | def macros 47 | @macros ||= {} 48 | end 49 | 50 | def add_include_directory(directory) 51 | return if directory_already_added?(:include, directory) 52 | 53 | add_compile_argument(Arguments::Include.new(directory)) 54 | end 55 | 56 | def add_library_directory(directory) 57 | return if directory_already_added?(:library_directory, directory) 58 | 59 | add_compile_argument(Arguments::LibraryDirectory.new(directory)) 60 | end 61 | 62 | def add_compile_argument(argument) 63 | return if runtime? 64 | 65 | add_argument(argument) 66 | end 67 | 68 | def add_runtime_argument(argument) 69 | return unless runtime? 70 | 71 | add_argument(argument) 72 | end 73 | 74 | def arguments 75 | @arguments ||= [] 76 | end 77 | 78 | def loaded_file_lists 79 | @loaded_file_lists ||= [] 80 | end 81 | 82 | private 83 | 84 | def runtime? 85 | options[:runtime] 86 | end 87 | 88 | def add_source_file?(file) 89 | target_ext?(file) && source_files.none?(file) 90 | end 91 | 92 | def target_ext?(file) 93 | return true unless @options.key?(:collect_ext) 94 | return true if @options[:collect_ext].empty? 95 | 96 | file.match_ext?(@options[:collect_ext]) 97 | end 98 | 99 | def add_library_file?(file) 100 | arguments.none? { |arg| arg.type == :library_file && arg.path == file } 101 | end 102 | 103 | def define_predefined_macros 104 | return unless options[:tool] 105 | 106 | list_name = File.join(__dir__, 'predefined_macros.yaml') 107 | list = YAML.safe_load_file(list_name, filename: list_name, symbolize_names: true) 108 | list[options[:tool]]&.each { |macro| add_macro_definition(macro, nil, true) } 109 | end 110 | 111 | def add_macro_definition(macro, value, predefined) 112 | name = macro.to_sym 113 | macros[name] = value || true 114 | add_macro_argument(name, value) unless predefined 115 | end 116 | 117 | def add_macro_argument(name, value) 118 | arguments.delete_if { |arg| macro_definition?(arg, name) } 119 | add_compile_argument(Arguments::Define.new(name, value)) 120 | end 121 | 122 | def remove_macro(macro) 123 | name = macro.to_sym 124 | arguments.reject! { |arg| macro_definition?(arg, name) } && macros.delete(name) 125 | end 126 | 127 | def macro_definition?(arg, name) 128 | arg.type == :define && arg.name == name 129 | end 130 | 131 | def directory_already_added?(type, path) 132 | arguments 133 | .any? { |argument| argument.type == type && argument.path == path } 134 | end 135 | 136 | def add_argument(argument) 137 | return unless argument.match_tool?(options[:tool]) 138 | 139 | arguments << argument 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/flgen/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class FLGenError < StandardError 5 | end 6 | 7 | class NoEntryError < FLGenError 8 | def initialize(path, location) 9 | message = 10 | "no such file or directory -- #{path} " \ 11 | "@#{location.path}:#{location.lineno}" 12 | super(message) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/flgen/file_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class FileList 5 | def initialize(context, path) 6 | @context = context 7 | @path = path 8 | @root_directories = extract_root 9 | @default_search_path = init_default_search_path 10 | end 11 | 12 | def default_search_path(**search_paths) 13 | @default_search_path.update(search_paths) 14 | end 15 | 16 | def reset_default_search_path(*target_types) 17 | target_types.each { |type| @default_search_path.delete(type) } 18 | end 19 | 20 | def file_list(path, from: nil, raise_error: true) 21 | load_file_list(path, from, raise_error, caller_location) 22 | end 23 | 24 | def source_file(path, from: nil, raise_error: true) 25 | add_entry(path, from, raise_error, __callee__, caller_location) 26 | end 27 | 28 | define_method(:library_file, instance_method(:source_file)) 29 | define_method(:include_directory, instance_method(:source_file)) 30 | define_method(:library_directory, instance_method(:source_file)) 31 | 32 | def find_files(patterns, from: nil, &block) 33 | glob_files(patterns, from, __callee__, caller_location) 34 | .then { |e| block_given? && (return e.each(&block)) || e.to_a } 35 | end 36 | 37 | def find_file(patterns, from: nil) 38 | glob_files(patterns, from, __callee__, caller_location) 39 | .first&.then { |f| block_given? && (return yield f) || f } 40 | end 41 | 42 | def file?(path, from: :current) 43 | !extract_path(path, from, __callee__, caller_location).nil? 44 | end 45 | 46 | define_method(:directory?, instance_method(:file?)) 47 | 48 | def define_macro(macro, value = nil) 49 | @context.define_macro(macro, value) 50 | end 51 | 52 | def undefine_macro(macro) 53 | @context.undefine_macro(macro) 54 | end 55 | 56 | def macro?(macro) 57 | @context.macros.key?(macro.to_sym) 58 | end 59 | 60 | alias_method :macro_defined?, :macro? 61 | 62 | def macro(name) 63 | @context.macros[name.to_sym] 64 | end 65 | 66 | def env?(name) 67 | ENV.key?(name.to_s) 68 | end 69 | 70 | def env(name) 71 | ENV.fetch(name.to_s, nil) 72 | end 73 | 74 | def target_tool?(tool) 75 | target_tool = @context.options[:tool] 76 | target_tool && tool.to_sym == target_tool || false 77 | end 78 | 79 | def compile_argument(argument, tool: nil) 80 | @context.add_compile_argument(Arguments::Generic.new(argument, tool)) 81 | end 82 | 83 | def runtime_argument(argument, tool: nil) 84 | @context.add_runtime_argument(Arguments::Generic.new(argument, tool)) 85 | end 86 | 87 | private 88 | 89 | def extract_root 90 | return nil if @path.empty? 91 | 92 | Pathname 93 | .new(@path).dirname.descend.select(&method(:repository_root?)).map(&:to_s) 94 | end 95 | 96 | def repository_root?(path) 97 | File.exist?(path.join('.git').to_s) 98 | end 99 | 100 | def init_default_search_path 101 | Hash.new { |_, key| key == :file_list ? :root : :current } 102 | end 103 | 104 | def load_file_list(path, from, raise_error, location) 105 | unless (extracted_path = extract_path(path, from, :file_list, location)) 106 | raise_no_entry_error(path, location, raise_error) 107 | return 108 | end 109 | 110 | # Need to File.realpath to resolve symblic link 111 | list_path = File.realpath(extracted_path) 112 | file_list_already_loaded?(list_path) && return 113 | 114 | @context.loaded_file_lists << list_path 115 | self.class.new(@context, list_path) 116 | .instance_eval(File.read(list_path), list_path) 117 | end 118 | 119 | def file_list_already_loaded?(path) 120 | @context.loaded_file_lists.include?(path) 121 | end 122 | 123 | def add_entry(path, from, raise_error, method_name, location) 124 | unless (extracted_path = extract_path(path, from, method_name, location)) 125 | raise_no_entry_error(path, location, raise_error) 126 | return 127 | end 128 | 129 | @context.__send__("add_#{method_name}", extracted_path) 130 | end 131 | 132 | def raise_no_entry_error(path, location, raise_error) 133 | raise_error && (raise NoEntryError.new(path, location)) 134 | end 135 | 136 | def glob_files(patterns, from, method_name, location) 137 | search_root('', from, method_name, location) 138 | .lazy.flat_map { |base| do_glob_files(patterns, base) } 139 | end 140 | 141 | def do_glob_files(patterns, base) 142 | Dir.glob(Array(patterns), base: base) 143 | .map { |path| File.join(base, path) } 144 | .select(&File.method(:file?)) 145 | end 146 | 147 | def caller_location 148 | caller_locations(2, 1).first 149 | end 150 | 151 | def extract_path(path, from, method_name, location) 152 | search_root(path, from, method_name, location) 153 | .map { |root| File.expand_path(path, root) } 154 | .find { |abs_path| exist_path?(abs_path, method_name) } 155 | end 156 | 157 | FROM_KEYWORDS = [:cwd, :current, :local_root, :root].freeze 158 | 159 | def search_root(path, from, method_name, location) 160 | search_path = from || @default_search_path[method_name] 161 | if absolute_path?(path) 162 | [''] 163 | elsif FROM_KEYWORDS.include?(search_path) 164 | search_root_specified_by_keyword(search_path, location) 165 | else 166 | [search_path] 167 | end 168 | end 169 | 170 | def absolute_path?(path) 171 | Pathname.new(path).absolute? 172 | end 173 | 174 | def search_root_specified_by_keyword(from_keyword, location) 175 | case from_keyword 176 | when :cwd then [Dir.pwd] 177 | when :current then [current_directory(location)] 178 | when :local_root then [@root_directories.last] 179 | else @root_directories 180 | end 181 | end 182 | 183 | def current_directory(location) 184 | # From Ruby 3.1 Thread::Backtrace::Location#absolute_path returns nil 185 | # for code string evaluated by eval methods 186 | # see https://github.com/ruby/ruby/commit/64ac984129a7a4645efe5ac57c168ef880b479b2 187 | path = location.absolute_path || location.path 188 | File.dirname(path) 189 | end 190 | 191 | METHODS_TARGETING_DIRECTORY = 192 | [:include_directory, :library_directory, :directory?].freeze 193 | 194 | def exist_path?(path, method_name) 195 | if METHODS_TARGETING_DIRECTORY.include?(method_name) 196 | File.directory?(path) 197 | else 198 | File.file?(path) 199 | end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/flgen/file_list_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class FileListFormatter < Formatter 5 | def format_header_line(line) 6 | "// #{line}" 7 | end 8 | 9 | def format_macro(macro, value) 10 | if value.nil? 11 | "+define+#{macro}" 12 | else 13 | "+define+#{macro}=#{value}" 14 | end 15 | end 16 | 17 | def format_include_directory(directory) 18 | "+incdir+#{directory}" 19 | end 20 | 21 | def format_libarary_directory(directory) 22 | "-y #{directory}" 23 | end 24 | 25 | def format_libarary_file(file) 26 | "-v #{file}" 27 | end 28 | 29 | def fomrat_argument(argument) 30 | argument 31 | end 32 | 33 | def format_file_path(path) 34 | path 35 | end 36 | 37 | Formatter.add_formatter(:filelist, self) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/flgen/file_list_xsim_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class FileListXsimFormatter < Formatter 5 | def format_header_line(line) 6 | "// #{line}" 7 | end 8 | 9 | def format_macro(macro, value) 10 | if value.nil? 11 | "-d #{macro}" 12 | else 13 | "-d #{macro}=#{value}" 14 | end 15 | end 16 | 17 | def format_include_directory(directory) 18 | "-i #{directory}" 19 | end 20 | 21 | def format_libarary_directory(directory) 22 | "-sourcelibdir #{directory}" 23 | end 24 | 25 | def format_libarary_file(file) 26 | "-sourcelibfile #{file}" 27 | end 28 | 29 | def fomrat_argument(argument) 30 | argument 31 | end 32 | 33 | def format_file_path(path) 34 | path 35 | end 36 | 37 | Formatter.add_formatter(:'filelist-xsim', self) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/flgen/formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class Formatter 5 | class << self 6 | def add_formatter(type, formatter) 7 | formatters[type] = formatter 8 | end 9 | 10 | def formatters 11 | @formatters ||= {} 12 | end 13 | end 14 | 15 | def initialize(context) 16 | @context = context 17 | @header_lines = [] 18 | end 19 | 20 | attr_reader :header_lines 21 | 22 | def output(io) 23 | print_header(io) 24 | print_macros(io) 25 | print_include_directoris(io) 26 | print_library_direcotries(io) 27 | print_library_files(io) 28 | print_arguments(io) 29 | print_source_files(io) 30 | end 31 | 32 | private 33 | 34 | def print_header(io) 35 | return unless print_header? 36 | 37 | header_lines.each do |line| 38 | print_value(io, :format_header_line, line) 39 | end 40 | end 41 | 42 | def format_header_line(_line) 43 | end 44 | 45 | def print_header? 46 | @context.options[:output] && 47 | @context.options[:print_header] && !@context.options[:source_file_only] 48 | end 49 | 50 | def print_macros(io) 51 | return if source_file_only? || no_arguments?(:define) 52 | 53 | pre_macros(io) 54 | each_argument(:define) do |argument| 55 | print_value(io, :format_macro, argument.name, argument.value) 56 | end 57 | post_macros(io) 58 | end 59 | 60 | def pre_macros(_io) 61 | end 62 | 63 | def format_macro(_name, _value) 64 | end 65 | 66 | def post_macros(_io) 67 | end 68 | 69 | def print_include_directoris(io) 70 | return if source_file_only? || no_arguments?(:include) 71 | 72 | pre_include_directories(io) 73 | each_argument(:include) do |argument| 74 | print_value(io, :format_include_directory, argument.path) 75 | end 76 | post_include_directories(io) 77 | end 78 | 79 | def pre_include_directories(_io) 80 | end 81 | 82 | def format_include_directory(_path) 83 | end 84 | 85 | def post_include_directories(_io) 86 | end 87 | 88 | def print_library_direcotries(io) 89 | return if source_file_only? || no_arguments?(:library_directory) 90 | 91 | pre_library_direcotries(io) 92 | each_argument(:library_directory) do |argument| 93 | print_value(io, :format_libarary_directory, argument.path) 94 | end 95 | post_library_direcotries(io) 96 | end 97 | 98 | def pre_library_direcotries(_io) 99 | end 100 | 101 | def format_libarary_directory(_path) 102 | end 103 | 104 | def post_library_direcotries(_io) 105 | end 106 | 107 | def print_library_files(io) 108 | return if source_file_only? || no_arguments?(:library_file) 109 | 110 | pre_library_files(io) 111 | each_argument(:library_file) do |argument| 112 | print_value(io, :format_libarary_file, argument.path) 113 | end 114 | post_library_files(io) 115 | end 116 | 117 | def pre_library_files(_io) 118 | end 119 | 120 | def format_libarary_file(_path) 121 | end 122 | 123 | def post_library_files(_io) 124 | end 125 | 126 | def print_arguments(io) 127 | return if source_file_only? || no_arguments?(:generic) 128 | 129 | pre_arguments(io) 130 | each_argument(:generic) do |argument| 131 | print_value(io, :fomrat_argument, argument.argument) 132 | end 133 | post_arguments(io) 134 | end 135 | 136 | def pre_arguments(io) 137 | end 138 | 139 | def fomrat_argument(_argument) 140 | end 141 | 142 | def post_arguments(io) 143 | end 144 | 145 | def source_file_only? 146 | @context.options[:source_file_only] 147 | end 148 | 149 | def print_source_files(io) 150 | return if no_source_files? 151 | 152 | pre_source_files(io) 153 | @context.source_files.each do |file| 154 | print_value(io, :format_file_path, file) 155 | end 156 | post_source_files(io) 157 | end 158 | 159 | def no_source_files? 160 | @context.source_files.empty? 161 | end 162 | 163 | def pre_source_files(_io) 164 | end 165 | 166 | def format_file_path(_path) 167 | end 168 | 169 | def post_source_files(_io) 170 | end 171 | 172 | def print_value(io, fomrtatter, *args) 173 | line = __send__(fomrtatter, *args) 174 | line && io.puts(line) 175 | end 176 | 177 | def no_arguments?(type) 178 | @context.arguments.none? { |argument| argument.type == type } 179 | end 180 | 181 | def each_argument(type, &block) 182 | @context.arguments.each do |argument| 183 | argument.type == type && block.call(argument) 184 | end 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /lib/flgen/predefined_macros.yaml: -------------------------------------------------------------------------------- 1 | vcs: 2 | - VCS 3 | design_compiler: 4 | - SYNTHESIS 5 | formality: 6 | - SYNTHESIS 7 | xcelium: 8 | - XCELIUM 9 | dsim: 10 | - DSIM 11 | vivado: 12 | - SYNTHESIS 13 | vivado_simulator: 14 | - XILINX_SIMULATOR 15 | -------------------------------------------------------------------------------- /lib/flgen/source_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class SourceFile 5 | def initialize(path, checksum = nil) 6 | @path = path 7 | @checksum = checksum 8 | end 9 | 10 | attr_reader :path 11 | alias_method :to_s, :path 12 | 13 | def ==(other) 14 | case other 15 | when SourceFile then path == other.path || checksum == other.checksum 16 | else path == other 17 | end 18 | end 19 | 20 | def match_ext?(ext_list) 21 | return false if ext_list.nil? || ext_list.empty? 22 | 23 | file_ext = File.extname(@path)[1..] 24 | ext_list.any? { |ext| (ext[0] == '.' && ext[1..] || ext) == file_ext } 25 | end 26 | 27 | def remove_ext(ext_list) 28 | return self unless match_ext?(ext_list) 29 | 30 | path_no_ext = Pathname.new(path).sub_ext('').to_s 31 | self.class.new(path_no_ext, checksum) 32 | end 33 | 34 | def checksum 35 | @checksum ||= Digest::MD5.digest(File.read(@path)) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/flgen/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | VERSION = '0.22.0' 5 | end 6 | -------------------------------------------------------------------------------- /lib/flgen/vivado_tcl_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FLGen 4 | class VivadoTCLFormatter < Formatter 5 | def format_header_line(line) 6 | "# #{line}" 7 | end 8 | 9 | def pre_macros(io) 10 | io.puts('set flgen_defines {}') 11 | end 12 | 13 | def format_macro(macro, value) 14 | if value.nil? 15 | "lappend flgen_defines \"#{macro}\"" 16 | else 17 | "lappend flgen_defines \"#{macro}=#{value}\"" 18 | end 19 | end 20 | 21 | def post_macros(io) 22 | io.puts('set_property verilog_define $flgen_defines [current_fileset]') 23 | end 24 | 25 | def pre_include_directories(io) 26 | io.puts('set flgen_include_directories {}') 27 | end 28 | 29 | def format_include_directory(directory) 30 | "lappend flgen_include_directories \"#{directory}\"" 31 | end 32 | 33 | def post_include_directories(io) 34 | io.puts('set_property include_dirs $flgen_include_directories [current_fileset]') 35 | end 36 | 37 | def pre_source_files(io) 38 | io.puts('set flgen_source_files {}') 39 | end 40 | 41 | def format_file_path(path) 42 | "lappend flgen_source_files \"#{path}\"" 43 | end 44 | 45 | def post_source_files(io) 46 | io.puts('add_files -fileset [current_fileset] $flgen_source_files') 47 | end 48 | 49 | Formatter.add_formatter(:'vivado-tcl', self) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /sample/bar/bar.list.rb: -------------------------------------------------------------------------------- 1 | define_macro :BAR_0 2 | define_macro :BAR_1, 1 3 | compile_argument '-bar_0', tool: :vcs 4 | runtime_argument '-bar_1', tool: :vcs 5 | 6 | library_directory 'bar_lib' 7 | source_file 'sample/bar/bar.sv', from: :root 8 | file_list 'baz/baz.list.rb', from: :current 9 | -------------------------------------------------------------------------------- /sample/bar/bar.sv: -------------------------------------------------------------------------------- 1 | module bar; 2 | endmodule 3 | -------------------------------------------------------------------------------- /sample/bar/bar_lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pezy-computing/flgen/653cc62f0ce6ff71f1e90eb86506d80eabff0617/sample/bar/bar_lib/.gitkeep -------------------------------------------------------------------------------- /sample/bar/baz/baz.list.rb: -------------------------------------------------------------------------------- 1 | include_directory '.' 2 | if target_tool? :xcelium 3 | compile_argument '-baz_0' 4 | runtime_argument '-baz_1' 5 | end 6 | unless macro_defined? :NO_BAZ 7 | source_file 'baz.sv' 8 | end 9 | -------------------------------------------------------------------------------- /sample/bar/baz/baz.sv: -------------------------------------------------------------------------------- 1 | module baz; 2 | endmodule 3 | -------------------------------------------------------------------------------- /sample/foo.list.rb: -------------------------------------------------------------------------------- 1 | compile_argument '-foo_0' 2 | runtime_argument '-foo_1' 3 | include_directory 'bar' 4 | source_file 'foo.sv' 5 | library_file 'foo_lib.sv' 6 | file_list 'sample/bar/bar.list.rb' 7 | -------------------------------------------------------------------------------- /sample/foo.sv: -------------------------------------------------------------------------------- 1 | module foo; 2 | endmodule 3 | -------------------------------------------------------------------------------- /sample/foo_lib.sv: -------------------------------------------------------------------------------- 1 | module foo_lib; 2 | endmodule 3 | -------------------------------------------------------------------------------- /spec/flgen/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe FLGen::CLI do 4 | let(:cli) do 5 | described_class.new 6 | end 7 | 8 | let(:files) do 9 | [ 10 | 'foo.sv', 11 | 'bar/bar.sv', 12 | 'bar/baz/baz.sv' 13 | ].map(&method(:expand_sample_path)) 14 | end 15 | 16 | let(:library_files) do 17 | [ 18 | 'foo_lib.sv' 19 | ].map(&method(:expand_sample_path)) 20 | end 21 | 22 | let(:macros) do 23 | ['BAR_0', 'BAR_1=1', 'NO_BAZ'] 24 | end 25 | 26 | let(:include_directories) do 27 | [ 28 | 'bar', 29 | 'bar/baz', 30 | 'foo' 31 | ].map(&method(:expand_sample_path)) 32 | end 33 | 34 | let(:library_directories) do 35 | [ 36 | 'bar/bar_lib' 37 | ].map(&method(:expand_sample_path)) 38 | end 39 | 40 | let(:compile_arguments) do 41 | ['-foo_0', '-bar_0', '-baz_0'] 42 | end 43 | 44 | let(:runtime_arguments) do 45 | ['-foo_1', '-bar_1', '-baz_1'] 46 | end 47 | 48 | let(:file_list) do 49 | File.join(FLGEN_ROOT, 'sample', 'foo.list.rb') 50 | end 51 | 52 | let(:io) do 53 | StringIO.new(+'') 54 | end 55 | 56 | def expand_sample_path(path) 57 | File.join(FLGEN_ROOT, 'sample', path) 58 | end 59 | 60 | describe 'ファイルリスト出力' do 61 | context '出力先が未指定の場合' do 62 | it '標準出力にファイルリストを出力する' do 63 | expect { cli.run(['--print-header', file_list]) }.to output(<<~OUT).to_stdout 64 | +define+#{macros[0]} 65 | +define+#{macros[1]} 66 | +incdir+#{include_directories[0]} 67 | +incdir+#{include_directories[1]} 68 | -y #{library_directories[0]} 69 | -v #{library_files[0]} 70 | #{compile_arguments[0]} 71 | #{files[0]} 72 | #{files[1]} 73 | #{files[2]} 74 | OUT 75 | end 76 | end 77 | 78 | context '出力先が指定されている場合' do 79 | let(:output) do 80 | 'out.f' 81 | end 82 | 83 | before do 84 | allow(File).to receive(:open).with(output, 'w').and_yield(io) 85 | end 86 | 87 | it 'filelist形式でファイルリストを書き出す' do 88 | cli.run(["--output=#{output}", file_list]) 89 | expect(io.string).to eq(<<~OUT) 90 | // flgen version #{FLGen::VERSION} 91 | // applied arguments 92 | // --output=#{output} 93 | // #{file_list} 94 | +define+#{macros[0]} 95 | +define+#{macros[1]} 96 | +incdir+#{include_directories[0]} 97 | +incdir+#{include_directories[1]} 98 | -y #{library_directories[0]} 99 | -v #{library_files[0]} 100 | #{compile_arguments[0]} 101 | #{files[0]} 102 | #{files[1]} 103 | #{files[2]} 104 | OUT 105 | end 106 | end 107 | 108 | context '--format=vivado-tclが指定された場合' do 109 | let(:output) do 110 | 'out.tcl' 111 | end 112 | 113 | before do 114 | allow(File).to receive(:open).with(output, 'w').and_yield(io) 115 | end 116 | 117 | it 'vivadoで読み込み可能なTCLを書き出す' do 118 | cli.run(["--output=#{output}", '--format=vivado-tcl', file_list]) 119 | expect(io.string).to eq(<<~TCL) 120 | # flgen version #{FLGen::VERSION} 121 | # applied arguments 122 | # --output=#{output} 123 | # --format=vivado-tcl 124 | # #{file_list} 125 | set flgen_defines {} 126 | lappend flgen_defines "#{macros[0]}" 127 | lappend flgen_defines "#{macros[1]}" 128 | set_property verilog_define $flgen_defines [current_fileset] 129 | set flgen_include_directories {} 130 | lappend flgen_include_directories "#{include_directories[0]}" 131 | lappend flgen_include_directories "#{include_directories[1]}" 132 | set_property include_dirs $flgen_include_directories [current_fileset] 133 | set flgen_source_files {} 134 | lappend flgen_source_files "#{files[0]}" 135 | lappend flgen_source_files "#{files[1]}" 136 | lappend flgen_source_files "#{files[2]}" 137 | add_files -fileset [current_fileset] $flgen_source_files 138 | TCL 139 | end 140 | end 141 | 142 | context '--format=filelist-xsimが指定された場合' do 143 | let(:output) do 144 | 'out.f' 145 | end 146 | 147 | before do 148 | allow(File).to receive(:open).with(output, 'w').and_yield(io) 149 | end 150 | 151 | it 'xsim対応のfilelist形式でファイルリストを書き出す' do 152 | cli.run(["--output=#{output}", '--format=filelist-xsim', file_list]) 153 | expect(io.string).to eq(<<~OUT) 154 | // flgen version #{FLGen::VERSION} 155 | // applied arguments 156 | // --output=#{output} 157 | // --format=filelist-xsim 158 | // #{file_list} 159 | -d #{macros[0]} 160 | -d #{macros[1]} 161 | -i #{include_directories[0]} 162 | -i #{include_directories[1]} 163 | -sourcelibdir #{library_directories[0]} 164 | -sourcelibfile #{library_files[0]} 165 | #{compile_arguments[0]} 166 | #{files[0]} 167 | #{files[1]} 168 | #{files[2]} 169 | OUT 170 | end 171 | end 172 | 173 | context '--no-print-headerが指定された場合' do 174 | let(:output) do 175 | 'out.f' 176 | end 177 | 178 | before do 179 | allow(File).to receive(:open).with(output, 'w').and_yield(io) 180 | end 181 | 182 | it 'ヘッダーを出力しない' do 183 | cli.run(['--no-print-header', "--output=#{output}", file_list]) 184 | expect(io.string).to eq(<<~OUT) 185 | +define+#{macros[0]} 186 | +define+#{macros[1]} 187 | +incdir+#{include_directories[0]} 188 | +incdir+#{include_directories[1]} 189 | -y #{library_directories[0]} 190 | -v #{library_files[0]} 191 | #{compile_arguments[0]} 192 | #{files[0]} 193 | #{files[1]} 194 | #{files[2]} 195 | OUT 196 | end 197 | end 198 | end 199 | 200 | context '--compileが指定された場合' do 201 | it 'コンパイル引数およびソースファイルを出力する' do 202 | expect { cli.run(['--compile', file_list]) }.to output(<<~OUT).to_stdout 203 | +define+#{macros[0]} 204 | +define+#{macros[1]} 205 | +incdir+#{include_directories[0]} 206 | +incdir+#{include_directories[1]} 207 | -y #{library_directories[0]} 208 | -v #{library_files[0]} 209 | #{compile_arguments[0]} 210 | #{files[0]} 211 | #{files[1]} 212 | #{files[2]} 213 | OUT 214 | end 215 | end 216 | 217 | context '--runtimeが指定された場合' do 218 | it 'ランタイム引数のみを出力する' do 219 | expect { 220 | cli.run(['--runtime', file_list]) 221 | }.to output(<<~OUT).to_stdout 222 | #{runtime_arguments[0]} 223 | OUT 224 | end 225 | end 226 | 227 | context '--toolが指定された場合' do 228 | it '指定されたツールに対応する引数を含むファイルリストを出力する' do 229 | expect { 230 | cli.run(['--compile', '--tool=vcs', file_list]) 231 | }.to output(<<~OUT).to_stdout 232 | +define+#{macros[0]} 233 | +define+#{macros[1]} 234 | +incdir+#{include_directories[0]} 235 | +incdir+#{include_directories[1]} 236 | -y #{library_directories[0]} 237 | -v #{library_files[0]} 238 | #{compile_arguments[0]} 239 | #{compile_arguments[1]} 240 | #{files[0]} 241 | #{files[1]} 242 | #{files[2]} 243 | OUT 244 | 245 | expect { 246 | cli.run(['--compile', '--tool=xcelium', file_list]) 247 | }.to output(<<~OUT).to_stdout 248 | +define+#{macros[0]} 249 | +define+#{macros[1]} 250 | +incdir+#{include_directories[0]} 251 | +incdir+#{include_directories[1]} 252 | -y #{library_directories[0]} 253 | -v #{library_files[0]} 254 | #{compile_arguments[0]} 255 | #{compile_arguments[2]} 256 | #{files[0]} 257 | #{files[1]} 258 | #{files[2]} 259 | OUT 260 | 261 | expect { 262 | cli.run(['--compile', '--tool=vcs', file_list]) 263 | }.to output(<<~OUT).to_stdout 264 | +define+#{macros[0]} 265 | +define+#{macros[1]} 266 | +incdir+#{include_directories[0]} 267 | +incdir+#{include_directories[1]} 268 | -y #{library_directories[0]} 269 | -v #{library_files[0]} 270 | #{compile_arguments[0]} 271 | #{compile_arguments[1]} 272 | #{files[0]} 273 | #{files[1]} 274 | #{files[2]} 275 | OUT 276 | 277 | expect { 278 | cli.run(['--runtime', '--tool=vcs', file_list]) 279 | }.to output(<<~OUT).to_stdout 280 | #{runtime_arguments[0]} 281 | #{runtime_arguments[1]} 282 | OUT 283 | 284 | expect { 285 | cli.run(['--runtime', '--tool=xcelium', file_list]) 286 | }.to output(<<~OUT).to_stdout 287 | #{runtime_arguments[0]} 288 | #{runtime_arguments[2]} 289 | OUT 290 | end 291 | end 292 | 293 | context '--source-file-onlyが指定された場合' do 294 | it 'ソースファイル部のみを出力する' do 295 | expect { cli.run(['--source-file-only', file_list]) }.to output(<<~OUT).to_stdout 296 | #{files[0]} 297 | #{files[1]} 298 | #{files[2]} 299 | OUT 300 | end 301 | end 302 | 303 | describe '--define-macroオプション' do 304 | it 'マクロを定義する' do 305 | expect { cli.run(['--define-macro=NO_BAZ', file_list]) }.to output(<<~OUT).to_stdout 306 | +define+#{macros[2]} 307 | +define+#{macros[0]} 308 | +define+#{macros[1]} 309 | +incdir+#{include_directories[0]} 310 | +incdir+#{include_directories[1]} 311 | -y #{library_directories[0]} 312 | -v #{library_files[0]} 313 | #{compile_arguments[0]} 314 | #{files[0]} 315 | #{files[1]} 316 | OUT 317 | end 318 | end 319 | 320 | describe '--include-directoryオプション' do 321 | it 'インクルードディレクトリを追加する' do 322 | expect { 323 | cli.run(["--include-directory=#{include_directories[2]}", file_list]) 324 | }.to output(<<~OUT).to_stdout 325 | +define+#{macros[0]} 326 | +define+#{macros[1]} 327 | +incdir+#{include_directories[2]} 328 | +incdir+#{include_directories[0]} 329 | +incdir+#{include_directories[1]} 330 | -y #{library_directories[0]} 331 | -v #{library_files[0]} 332 | #{compile_arguments[0]} 333 | #{files[0]} 334 | #{files[1]} 335 | #{files[2]} 336 | OUT 337 | end 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /spec/flgen/context_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe FLGen::Context do 4 | let(:options) do 5 | {} 6 | end 7 | 8 | let(:context) do 9 | described_class.new(options) 10 | end 11 | 12 | def match_source_file(path) 13 | have_attributes(path: path) 14 | end 15 | 16 | def match_argument(type, **attributes) 17 | have_attributes(type: type, **attributes) 18 | end 19 | 20 | describe '#add_source_file' do 21 | let(:file_contents) do 22 | contents = [] 23 | contents << <<~'CODE' 24 | module hoge; 25 | endmodule 26 | CODE 27 | contents << <<~'CODE' 28 | module fuga; 29 | endmodule 30 | CODE 31 | contents << <<~'CODE' 32 | module piyo; 33 | endmodule 34 | CODE 35 | contents 36 | end 37 | 38 | it '指定したソースファイルを追加する' do 39 | allow(File).to receive(:read).with('/foo/bar/hoge.sv').and_return(file_contents[0]) 40 | allow(File).to receive(:read).with('/baz/qux/fuga.sv').and_return(file_contents[1]) 41 | 42 | context.add_source_file('/foo/bar/hoge.sv') 43 | context.add_source_file('/baz/qux/fuga.sv') 44 | 45 | expect(context.source_files).to match([ 46 | match_source_file('/foo/bar/hoge.sv'), 47 | match_source_file('/baz/qux/fuga.sv') 48 | ]) 49 | end 50 | 51 | context 'options[:runtime]がtrueの場合' do 52 | specify 'ソースファイルの追加は行わない' do 53 | options[:runtime] = true 54 | 55 | expect { 56 | context.add_source_file('/foo/bar/hoge.sv') 57 | context.add_source_file('/baz/qux/fuga.sv') 58 | }.not_to change { context.source_files.size } 59 | end 60 | end 61 | 62 | context 'options[:collect_ext]が指定されている場合' do 63 | it '指定した拡張子を持つファイルだけを追加する' do 64 | options[:collect_ext] = ['gz', 'bz2'] 65 | 66 | allow(File).to receive(:read).with('/foo/bar/hoge.sv.gz').and_return(file_contents[0]) 67 | allow(File).to receive(:read).with('/foo/bar/fuga.sv.bz2').and_return(file_contents[1]) 68 | 69 | context.add_source_file('/foo/bar/hoge.sv.gz') 70 | context.add_source_file('/foo/bar/hoge.sv') 71 | context.add_source_file('/foo/bar/fuga.sv.bz2') 72 | context.add_source_file('/foo/bar/fuga.sv') 73 | 74 | expect(context.source_files).to match([ 75 | match_source_file('/foo/bar/hoge.sv.gz'), 76 | match_source_file('/foo/bar/fuga.sv.bz2') 77 | ]) 78 | end 79 | end 80 | 81 | context 'options[:rm_ext]が指定されている場合' do 82 | it '指定された拡張子を削除して、追加する' do 83 | options[:rm_ext] = ['gz', 'bz2'] 84 | 85 | allow(File).to receive(:read).with('/foo/bar/hoge.sv.gz').and_return(file_contents[0]) 86 | allow(File).to receive(:read).with('/foo/bar/fuga.sv.bz2').and_return(file_contents[1]) 87 | allow(File).to receive(:read).with('/foo/bar/piyo.sv').and_return(file_contents[2]) 88 | 89 | context.add_source_file('/foo/bar/hoge.sv.gz') 90 | context.add_source_file('/foo/bar/fuga.sv.bz2') 91 | context.add_source_file('/foo/bar/piyo.sv') 92 | 93 | expect(context.source_files).to match([ 94 | match_source_file('/foo/bar/hoge.sv'), 95 | match_source_file('/foo/bar/fuga.sv'), 96 | match_source_file('/foo/bar/piyo.sv') 97 | ]) 98 | end 99 | end 100 | 101 | context '同一のファイルが既に追加されている場合' do 102 | it '当該ファイルは追加しない' do 103 | allow(File).to receive(:read).with('/foo/bar/fizz/buzz/hoge.sv').and_return(file_contents[0]) 104 | allow(File).to receive(:read).with('/foo/bar/fizz/buzz/fuga.sv').and_return(file_contents[1]) 105 | allow(File).to receive(:read).with('/baz/qux/fizz/buzz/hoge.sv').and_return(file_contents[0]) 106 | allow(File).to receive(:read).with('/baz/qux/fizz/buzz/fuga.sv').and_return(file_contents[2]) 107 | allow(File).to receive(:read).with('/foo/bar/fizz/buzz/hoge.sv').and_return(file_contents[0]) 108 | 109 | context.add_source_file('/foo/bar/fizz/buzz/hoge.sv') 110 | context.add_source_file('/foo/bar/fizz/buzz/fuga.sv') 111 | context.add_source_file('/baz/qux/fizz/buzz/hoge.sv') 112 | context.add_source_file('/baz/qux/fizz/buzz/fuga.sv') 113 | context.add_source_file('/foo/bar/fizz/buzz/hoge.sv') 114 | 115 | expect(context.source_files).to match([ 116 | match_source_file('/foo/bar/fizz/buzz/hoge.sv'), 117 | match_source_file('/foo/bar/fizz/buzz/fuga.sv'), 118 | match_source_file('/baz/qux/fizz/buzz/fuga.sv') 119 | ]) 120 | end 121 | end 122 | end 123 | 124 | describe '#add_library_file' do 125 | let(:file_contents) do 126 | contents = [] 127 | contents << <<~'CODE' 128 | module hoge; 129 | endmodule 130 | CODE 131 | contents << <<~'CODE' 132 | module fuga; 133 | endmodule 134 | CODE 135 | contents << <<~'CODE' 136 | module piyo; 137 | endmodule 138 | CODE 139 | contents 140 | end 141 | 142 | it '指定したライブラリファイルを追加する' do 143 | allow(File).to receive(:read).with('/foo/bar/hoge.sv').and_return(file_contents[0]) 144 | allow(File).to receive(:read).with('/baz/qux/fuga.sv').and_return(file_contents[1]) 145 | 146 | context.add_library_file('/foo/bar/hoge.sv') 147 | context.add_library_file('/baz/qux/fuga.sv') 148 | 149 | expect(context.arguments).to match([ 150 | match_argument(:library_file, path: match_source_file('/foo/bar/hoge.sv')), 151 | match_argument(:library_file, path: match_source_file('/baz/qux/fuga.sv')) 152 | ]) 153 | end 154 | 155 | context 'options[:runtime]がtrueの場合' do 156 | specify 'ライブラリファイルの追加は行わない' do 157 | options[:runtime] = true 158 | 159 | expect { 160 | context.add_library_file('/foo/bar/hoge.sv') 161 | context.add_library_file('/baz/qux/fuga.sv') 162 | }.not_to change { context.arguments.size } 163 | end 164 | end 165 | 166 | context '同一のファイルが既に追加されている場合' do 167 | it '当該ファイルは追加しない' do 168 | allow(File).to receive(:read).with('/foo/bar/fizz/buzz/hoge.sv').and_return(file_contents[0]) 169 | allow(File).to receive(:read).with('/foo/bar/fizz/buzz/fuga.sv').and_return(file_contents[1]) 170 | allow(File).to receive(:read).with('/baz/qux/fizz/buzz/hoge.sv').and_return(file_contents[0]) 171 | allow(File).to receive(:read).with('/baz/qux/fizz/buzz/fuga.sv').and_return(file_contents[2]) 172 | allow(File).to receive(:read).with('/foo/bar/fizz/buzz/hoge.sv').and_return(file_contents[0]) 173 | 174 | context.add_library_file('/foo/bar/fizz/buzz/hoge.sv') 175 | context.add_library_file('/foo/bar/fizz/buzz/fuga.sv') 176 | context.add_library_file('/baz/qux/fizz/buzz/hoge.sv') 177 | context.add_library_file('/baz/qux/fizz/buzz/fuga.sv') 178 | context.add_library_file('/foo/bar/fizz/buzz/hoge.sv') 179 | 180 | expect(context.arguments).to match([ 181 | match_argument(:library_file, path: match_source_file('/foo/bar/fizz/buzz/hoge.sv')), 182 | match_argument(:library_file, path: match_source_file('/foo/bar/fizz/buzz/fuga.sv')), 183 | match_argument(:library_file, path: match_source_file('/baz/qux/fizz/buzz/fuga.sv')) 184 | ]) 185 | end 186 | end 187 | end 188 | 189 | describe '#define_macro' do 190 | it 'マクロを定義する' do 191 | context.define_macro(:FOO) 192 | context.define_macro('BAR=1') 193 | expect(context.macros).to match(FOO: true, BAR: '1') 194 | end 195 | 196 | it 'コンパイル引数として追加する' do 197 | context.define_macro(:FOO) 198 | context.define_macro('BAR=1') 199 | 200 | expect(context.arguments).to match([ 201 | match_argument(:define, name: :FOO, value: be_nil), 202 | match_argument(:define, name: :BAR, value: '1') 203 | ]) 204 | end 205 | 206 | context 'options[:runtme]がtrueの場合' do 207 | specify 'マクロ名の追加のみ行う' do 208 | options[:runtime] = true 209 | 210 | expect { 211 | context.define_macro(:FOO) 212 | context.define_macro('BAR=1') 213 | }.not_to change { context.arguments.size } 214 | expect(context.macros).to match(FOO: true, BAR: '1') 215 | end 216 | end 217 | 218 | context '同名のマクロが再定義された場合' do 219 | it '値を上書きする' do 220 | context.define_macro(:FOO) 221 | context.define_macro('BAR=1') 222 | expect(context.macros).to match(FOO: true, BAR: '1') 223 | expect(context.arguments).to match([ 224 | match_argument(:define, name: :FOO, value: be_nil), 225 | match_argument(:define, name: :BAR, value: '1') 226 | ]) 227 | 228 | context.define_macro('FOO=2') 229 | expect(context.macros).to match(FOO: '2', BAR: '1') 230 | expect(context.arguments).to match([ 231 | match_argument(:define, name: :BAR, value: '1'), 232 | match_argument(:define, name: :FOO, value: '2') 233 | ]) 234 | 235 | context.define_macro('BAR') 236 | expect(context.macros).to match(FOO: '2', BAR: true) 237 | expect(context.arguments).to match([ 238 | match_argument(:define, name: :FOO, value: '2'), 239 | match_argument(:define, name: :BAR, value: be_nil) 240 | ]) 241 | end 242 | end 243 | end 244 | 245 | describe '#undefine_macro' do 246 | it '定義済みマクロを無効化する' do 247 | context.define_macro(:FOO) 248 | context.define_macro('BAR=1') 249 | context.define_macro(:BAZ) 250 | 251 | context.undefine_macro(:QUX) 252 | context.undefine_macro(:BAZ) 253 | context.undefine_macro(:FOO) 254 | 255 | expect(context.macros).to match(BAR: '1') 256 | end 257 | 258 | specify '事前定義済みマクロは無効化しない' do 259 | options[:tool] = :vcs 260 | 261 | context.define_macro(:FOO) 262 | context.define_macro(:BAR) 263 | context.undefine_macro(:FOO) 264 | context.undefine_macro(:VCS) 265 | 266 | expect(context.macros).to match(VCS: true, BAR: true) 267 | end 268 | end 269 | 270 | describe '事前定義済みマクロ' do 271 | context '対象ツールがVCSの場合' do 272 | specify ':VCSが定義される' do 273 | options[:tool] = :vcs 274 | expect(context.macros).to match(VCS: true) 275 | end 276 | end 277 | 278 | context '対象ツールがDesign Compilerの場合' do 279 | specify 'SYNTHESISが定義される' do 280 | options[:tool] = :design_compiler 281 | expect(context.macros).to match(SYNTHESIS: true) 282 | end 283 | end 284 | 285 | context '対象ツールがFormalityの場合' do 286 | specify 'SYNTHESISが定義される' do 287 | options[:tool] = :formality 288 | expect(context.macros).to match(SYNTHESIS: true) 289 | end 290 | end 291 | 292 | context '対象ツールがXceliumの場合' do 293 | specify ':XCELIUMが定義される' do 294 | options[:tool] = :xcelium 295 | expect(context.macros).to match(XCELIUM: true) 296 | end 297 | end 298 | 299 | context '対象ツールがDSimの場合' do 300 | specify ':DSIMが定義される' do 301 | options[:tool] = :dsim 302 | expect(context.macros).to match(DSIM: true) 303 | end 304 | end 305 | 306 | context '対象ツールがVivadoの場合' do 307 | specify 'SYNTHESISが定義される' do 308 | options[:tool] = :vivado 309 | expect(context.macros).to match(SYNTHESIS: true) 310 | end 311 | end 312 | 313 | context '対象ツールがVivado simulatorの場合' do 314 | specify 'SYNTHESISが定義される' do 315 | options[:tool] = :vivado_simulator 316 | expect(context.macros).to match(XILINX_SIMULATOR: true) 317 | end 318 | end 319 | end 320 | 321 | describe '#add_include_directory' do 322 | let(:directories) do 323 | ['/foo/bar', '/fizz/buzz'] 324 | end 325 | 326 | it 'インクルードパスをコンパイル引数として追加する' do 327 | context.add_include_directory(directories[0]) 328 | context.add_include_directory(directories[1]) 329 | 330 | expect(context.arguments).to match([ 331 | match_argument(:include, path: directories[0]), 332 | match_argument(:include, path: directories[1]) 333 | ]) 334 | end 335 | 336 | context 'options[:runtme]がtrueの場合' do 337 | specify 'インクルードパスの追加は行わない' do 338 | options[:runtime] = true 339 | 340 | expect { 341 | context.add_include_directory(directories[0]) 342 | context.add_include_directory(directories[1]) 343 | }.not_to change { context.arguments.size } 344 | end 345 | end 346 | 347 | context '追加済みのディレクトリが再度指定された場合' do 348 | specify '追加済みのディレクトリは再度追加しない' do 349 | context.add_include_directory(directories[0]) 350 | context.add_include_directory(directories[0]) 351 | context.add_include_directory(directories[1]) 352 | 353 | expect(context.arguments).to match([ 354 | match_argument(:include, path: directories[0]), 355 | match_argument(:include, path: directories[1]) 356 | ]) 357 | end 358 | end 359 | end 360 | 361 | describe '#add_library_directory' do 362 | let(:directories) do 363 | ['/foo/bar', '/fizz/buzz'] 364 | end 365 | 366 | it 'ライブラリパスをコンパイル引数として追加する' do 367 | context.add_library_directory(directories[0]) 368 | context.add_library_directory(directories[1]) 369 | 370 | expect(context.arguments).to match([ 371 | match_argument(:library_directory, path: directories[0]), 372 | match_argument(:library_directory, path: directories[1]) 373 | ]) 374 | end 375 | 376 | context 'options[:runtme]がtrueの場合' do 377 | specify 'ライブラリパスの追加は行わない' do 378 | options[:runtime] = true 379 | 380 | expect { 381 | context.add_library_directory(directories[0]) 382 | context.add_library_directory(directories[1]) 383 | }.not_to change { context.arguments.size } 384 | end 385 | end 386 | 387 | context '追加済みのディレクトリが再度指定された場合' do 388 | specify '追加済みのディレクトリは再度追加しない' do 389 | context.add_library_directory(directories[0]) 390 | context.add_library_directory(directories[0]) 391 | context.add_library_directory(directories[1]) 392 | 393 | expect(context.arguments).to match([ 394 | match_argument(:library_directory, path: directories[0]), 395 | match_argument(:library_directory, path: directories[1]) 396 | ]) 397 | end 398 | end 399 | end 400 | 401 | describe '#add_compile_argument' do 402 | let(:compile_arguments) do 403 | [ 404 | FLGen::Arguments::Generic.new('-foo', nil), 405 | FLGen::Arguments::Generic.new('-bar', nil), 406 | FLGen::Arguments::Generic.new('-baz', :vcs), 407 | FLGen::Arguments::Generic.new('-qux', :xcelium) 408 | ] 409 | end 410 | 411 | it 'ツール指定がない引数を追加する' do 412 | context.add_compile_argument(compile_arguments[0]) 413 | context.add_compile_argument(compile_arguments[1]) 414 | context.add_compile_argument(compile_arguments[2]) 415 | context.add_compile_argument(compile_arguments[3]) 416 | 417 | expect(context.arguments).to match([ 418 | have_attributes(argument: '-foo', tool: be_nil), 419 | have_attributes(argument: '-bar', tool: be_nil) 420 | ]) 421 | end 422 | 423 | context 'options[:runtime]がtrueの場合' do 424 | specify '引数の追加は行わない' do 425 | options[:runtime] = true 426 | 427 | expect { 428 | context.add_compile_argument(compile_arguments[0]) 429 | context.add_compile_argument(compile_arguments[1]) 430 | context.add_compile_argument(compile_arguments[2]) 431 | context.add_compile_argument(compile_arguments[3]) 432 | }.not_to change { context.arguments.size } 433 | end 434 | end 435 | 436 | context 'options[:tool]の指定がある場合' do 437 | it 'ツール指定がない引数と、指定されたツールに対応する引数を追加する' do 438 | options[:tool] = :vcs 439 | 440 | context.add_compile_argument(compile_arguments[0]) 441 | context.add_compile_argument(compile_arguments[1]) 442 | context.add_compile_argument(compile_arguments[2]) 443 | context.add_compile_argument(compile_arguments[3]) 444 | 445 | expect(context.arguments).to match([ 446 | have_attributes(argument: '-foo', tool: be_nil), 447 | have_attributes(argument: '-bar', tool: be_nil), 448 | have_attributes(argument: '-baz', tool: :vcs) 449 | ]) 450 | end 451 | end 452 | end 453 | 454 | describe '#add_runtime_argument' do 455 | let(:runtime_arguments) do 456 | [ 457 | FLGen::Arguments::Generic.new('-foo', nil), 458 | FLGen::Arguments::Generic.new('-bar', nil), 459 | FLGen::Arguments::Generic.new('-baz', :vcs), 460 | FLGen::Arguments::Generic.new('-qux', :xcelium) 461 | ] 462 | end 463 | 464 | it 'ツール指定のない引数を追加する' do 465 | options[:runtime] = true 466 | 467 | context.add_runtime_argument(runtime_arguments[0]) 468 | context.add_runtime_argument(runtime_arguments[1]) 469 | context.add_runtime_argument(runtime_arguments[2]) 470 | context.add_runtime_argument(runtime_arguments[3]) 471 | 472 | expect(context.arguments).to match([ 473 | have_attributes(argument: '-foo', tool: be_nil), 474 | have_attributes(argument: '-bar', tool: be_nil) 475 | ]) 476 | end 477 | 478 | context 'options[:runtime]が未指定の場合' do 479 | specify '引数の追加は行わない' do 480 | expect { 481 | context.add_runtime_argument(runtime_arguments[0]) 482 | context.add_runtime_argument(runtime_arguments[1]) 483 | context.add_runtime_argument(runtime_arguments[2]) 484 | context.add_runtime_argument(runtime_arguments[3]) 485 | }.not_to change { context.arguments.size } 486 | end 487 | end 488 | 489 | context 'options[:runtime]がfalseの場合' do 490 | specify '引数の追加は行わない' do 491 | options[:runtime] = false 492 | 493 | expect { 494 | context.add_runtime_argument(runtime_arguments[0]) 495 | context.add_runtime_argument(runtime_arguments[1]) 496 | context.add_runtime_argument(runtime_arguments[2]) 497 | context.add_runtime_argument(runtime_arguments[3]) 498 | }.not_to change { context.arguments.size } 499 | end 500 | end 501 | 502 | context 'options[:tool]の指定がある場合' do 503 | it 'ツール指定がないと、指定されたツールに対応する引数を追加する' do 504 | options[:runtime] = true 505 | options[:tool] = :xcelium 506 | 507 | context.add_runtime_argument(runtime_arguments[0]) 508 | context.add_runtime_argument(runtime_arguments[1]) 509 | context.add_runtime_argument(runtime_arguments[2]) 510 | context.add_runtime_argument(runtime_arguments[3]) 511 | 512 | expect(context.arguments).to match([ 513 | have_attributes(argument: '-foo', tool: be_nil), 514 | have_attributes(argument: '-bar', tool: be_nil), 515 | have_attributes(argument: '-qux', tool: :xcelium) 516 | ]) 517 | end 518 | end 519 | end 520 | end 521 | -------------------------------------------------------------------------------- /spec/flgen/file_list_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe FLGen::FileList do 4 | let(:path) do 5 | '/foo/bar/baz/fizz/buzz/list.rb' 6 | end 7 | 8 | let(:directories) do 9 | Pathname 10 | .new(path).dirname 11 | .descend.map(&:to_s) 12 | end 13 | 14 | let(:root_directories) do 15 | [ 16 | '/foo/bar', 17 | '/foo/bar/baz/fizz' 18 | ] 19 | end 20 | 21 | let(:non_root_directories) do 22 | directories - root_directories 23 | end 24 | 25 | let(:context) do 26 | double('context', loaded_file_lists: [], options: {}, macros: {}) 27 | end 28 | 29 | before do 30 | [ 31 | '/', 32 | '/foo', 33 | '/foo/bar', 34 | '/foo/bar/baz', 35 | '/foo/bar/baz/fizz', 36 | '/foo/bar/baz/fizz/buzz' 37 | ].each do |dir| 38 | git_dir = File.join(dir, '.git') 39 | return_value = root_directories.include?(dir) 40 | allow(File).to receive(:exist?).with(git_dir).and_return(return_value) 41 | end 42 | end 43 | 44 | before do 45 | allow(File).to receive(:realpath) { |path| path } 46 | end 47 | 48 | def mock_cwd 49 | allow(Dir).to receive(:pwd).and_return(__dir__) 50 | end 51 | 52 | describe '#file_list' do 53 | let(:list_name) do 54 | 'hoge/fuga.rb' 55 | end 56 | 57 | let(:file_list_content) do 58 | <<~'FILE_LIST' 59 | source_file 'piyo.sv' 60 | FILE_LIST 61 | end 62 | 63 | let(:source_file_name) do 64 | 'piyo.sv' 65 | end 66 | 67 | def setup_expectation(root, list_name, *invalid_roots) 68 | new_list = double('file list') 69 | path = 70 | if root.empty? 71 | list_name 72 | else 73 | File.join(root, list_name) 74 | end 75 | allow(File).to receive(:file?).with(path).and_return(true) 76 | allow(File).to receive(:read).with(path).and_return(file_list_content) 77 | 78 | expect(described_class).to receive(:new).with(equal(context), path).once.and_return(new_list) 79 | expect(new_list).to receive(:source_file).with(source_file_name) 80 | 81 | invalid_roots.each do |invalid_root| 82 | allow(File).to receive(:file?).with(File.join(invalid_root, list_name)).and_return(false) 83 | end 84 | end 85 | 86 | it 'ルートディレクトリ中を検索し、指定されたファイルリストを読み出す' do 87 | file_list = described_class.new(context, path) 88 | list_paths = root_directories.map { |dir| File.join(dir, list_name) } 89 | 90 | setup_expectation(root_directories[0], list_name) 91 | file_list.file_list(list_name) 92 | 93 | setup_expectation(root_directories[1], list_name, root_directories[0]) 94 | file_list.file_list(list_name) 95 | end 96 | 97 | context '絶対パスで指定された場合' do 98 | it '指定されたファイルリストをそのまま読み出す' do 99 | file_list = described_class.new(context, '') 100 | list_path = File.join(root_directories[0], list_name) 101 | 102 | setup_expectation('', list_path) 103 | file_list.file_list(list_path) 104 | end 105 | end 106 | 107 | context 'from: :rootが指定された場合' do 108 | it 'ルートディレクトリ中を検索し、指定されたファイルリストを読み出す' do 109 | file_list = described_class.new(context, path) 110 | 111 | setup_expectation(root_directories[0], list_name) 112 | file_list.file_list(list_name) 113 | 114 | setup_expectation(root_directories[1], list_name, root_directories[0]) 115 | file_list.file_list(list_name) 116 | end 117 | end 118 | 119 | context 'from: :local_rootが指定された場合' do 120 | it '直近のリポジトリルートから検索を行う' do 121 | file_list = described_class.new(context, path) 122 | 123 | setup_expectation(root_directories.last, list_name) 124 | file_list.file_list(list_name, from: :local_root) 125 | end 126 | end 127 | 128 | context 'from: :currentが指定された場合' do 129 | it '呼び出し元を起点として、指定されたファイルリストを読み出す' do 130 | file_list = described_class.new(context, path) 131 | 132 | setup_expectation(__dir__, list_name) 133 | file_list.file_list(list_name, from: :current) 134 | end 135 | end 136 | 137 | context 'from: :cwdが指定された場合' do 138 | it '実行ディレクトリを起点として、指定されたファイルリストを読み出す' do 139 | file_list = described_class.new(context, path) 140 | 141 | mock_cwd 142 | setup_expectation(__dir__, list_name) 143 | file_list.file_list(list_name, from: :cwd) 144 | end 145 | end 146 | 147 | context 'from: でベースディレクトリが指定された場合' do 148 | it '指定されたファイルリストをベースディレクトリから読み出す' do 149 | base = non_root_directories.sample 150 | file_list = described_class.new(context, path) 151 | 152 | setup_expectation(base, list_name) 153 | file_list.file_list(list_name, from: base) 154 | end 155 | end 156 | 157 | specify '#default_search_pathでfrom:未指定時の挙動を変更できる' do 158 | file_list = described_class.new(context, path) 159 | 160 | context.loaded_file_lists.clear 161 | setup_expectation(root_directories[0], list_name) 162 | file_list.default_search_path(file_list: :root) 163 | file_list.file_list(list_name) 164 | 165 | context.loaded_file_lists.clear 166 | setup_expectation(root_directories.last, list_name) 167 | file_list.default_search_path(file_list: :local_root) 168 | file_list.file_list(list_name) 169 | 170 | context.loaded_file_lists.clear 171 | setup_expectation(root_directories.last, list_name) 172 | file_list.default_search_path(file_list: :local_root) 173 | file_list.file_list(list_name) 174 | 175 | context.loaded_file_lists.clear 176 | setup_expectation(__dir__, list_name) 177 | file_list.default_search_path(file_list: :current) 178 | file_list.file_list(list_name) 179 | 180 | mock_cwd 181 | context.loaded_file_lists.clear 182 | setup_expectation(__dir__, list_name) 183 | file_list.default_search_path(file_list: :cwd) 184 | file_list.file_list(list_name) 185 | 186 | context.loaded_file_lists.clear 187 | base = non_root_directories.sample 188 | setup_expectation(base, list_name) 189 | file_list.default_search_path(file_list: base) 190 | file_list.file_list(list_name) 191 | 192 | context.loaded_file_lists.clear 193 | setup_expectation(root_directories[1], list_name, root_directories[0]) 194 | file_list.reset_default_search_path(:file_list) 195 | file_list.file_list(list_name) 196 | end 197 | 198 | context '複数回同じファイルリストが指定された場合' do 199 | specify '2回目以降は読み込まない' do 200 | file_list = described_class.new(context, path) 201 | 202 | setup_expectation(root_directories[0], list_name) 203 | file_list.file_list(list_name) 204 | file_list.file_list(list_name) 205 | 206 | setup_expectation(__dir__, list_name) 207 | file_list.file_list(list_name, from: :current) 208 | file_list.file_list(list_name, from: :current) 209 | 210 | expect(context.loaded_file_lists).to match([ 211 | File.join(root_directories[0], list_name), 212 | File.join(__dir__, list_name) 213 | ]) 214 | end 215 | end 216 | 217 | context '指定したファイルリストが存在しない場合' do 218 | it 'LoadErrorを起こす' do 219 | file_list = described_class.new(context, path) 220 | [*root_directories, __dir__].each do |root| 221 | allow(File).to receive(:file?).with(File.join(root, list_name)).and_return(false) 222 | end 223 | 224 | expect { 225 | file_list.file_list(list_name) 226 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{list_name} @#{__FILE__}:#{__LINE__ - 1}" 227 | 228 | expect { 229 | file_list.file_list(list_name, from: :current) 230 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{list_name} @#{__FILE__}:#{__LINE__ - 1}" 231 | end 232 | end 233 | 234 | context '指定したファイルリストが存在せず、raise_error:falseが指定されている場合' do 235 | it 'エラーは起こさない' do 236 | file_list = described_class.new(context, path) 237 | [*root_directories, __dir__].each do |root| 238 | allow(File).to receive(:file?).with(File.join(root, list_name)).and_return(false) 239 | end 240 | 241 | expect { 242 | file_list.file_list(list_name, raise_error: false) 243 | }.not_to raise_error 244 | 245 | expect { 246 | file_list.file_list(list_name, from: :current, raise_error: false) 247 | }.not_to raise_error 248 | end 249 | end 250 | end 251 | 252 | describe '#source_file' do 253 | let(:source_file_name) do 254 | 'piyo.sv' 255 | end 256 | 257 | def setup_expectation(root, file_name, *invalid_roots) 258 | path = 259 | if root.empty? 260 | file_name 261 | else 262 | File.join(root, file_name) 263 | end 264 | 265 | allow(File).to receive(:file?).with(path).and_return(true) 266 | expect(context).to receive(:add_source_file).with(path) 267 | 268 | invalid_roots.each do |invalid_root| 269 | allow(File).to receive(:file?).with(File.join(invalid_root, file_name)).and_return(false) 270 | end 271 | end 272 | 273 | it '呼び出し元を起点として、contextにソースファイルを追加する' do 274 | file_list = described_class.new(context, path) 275 | 276 | setup_expectation(__dir__, source_file_name) 277 | file_list.source_file(source_file_name) 278 | end 279 | 280 | context '絶対パスで指定された場合' do 281 | it '指定されたソースファイルをcontextにそのまま追加する' do 282 | file_list = described_class.new(context, '') 283 | path = File.join(__dir__, source_file_name) 284 | 285 | setup_expectation('', path) 286 | file_list.source_file(path) 287 | end 288 | end 289 | 290 | context 'from: :rootが指定された場合' do 291 | it 'ルートディレクトリ中を検索し、contextにソースファイルを追加する' do 292 | file_list = described_class.new(context, path) 293 | 294 | setup_expectation(root_directories[0], source_file_name) 295 | file_list.source_file(source_file_name, from: :root) 296 | 297 | setup_expectation(root_directories[1], source_file_name, root_directories[0]) 298 | file_list.source_file(source_file_name, from: :root) 299 | end 300 | end 301 | 302 | context 'from: :local_rootが指定された場合' do 303 | it '直近のリポジトリルートから検索を行う' do 304 | file_path = File.join(root_directories.last, source_file_name) 305 | file_list = described_class.new(context, path) 306 | 307 | setup_expectation(root_directories.last, source_file_name) 308 | file_list.source_file(source_file_name, from: :local_root) 309 | end 310 | end 311 | 312 | context 'from: currentが指定された場合' do 313 | it '呼び出し元を起点として、contextにソースファイルを追加する' do 314 | file_list = described_class.new(context, path) 315 | 316 | setup_expectation(__dir__, source_file_name) 317 | file_list.source_file(source_file_name, from: :current) 318 | end 319 | end 320 | 321 | context 'from: :cwdが指定された場合' do 322 | it '実行ディレクトリを起点として、contextにソースファイルを追加する' do 323 | file_list = described_class.new(context, path) 324 | 325 | mock_cwd 326 | setup_expectation(__dir__, source_file_name) 327 | file_list.source_file(source_file_name, from: :cwd) 328 | end 329 | end 330 | 331 | context 'from:でベースディレクトリが指定された場合' do 332 | it '指定されたベースディレクトリから、contextにソースファイルを追加する' do 333 | file_list = described_class.new(context, path) 334 | 335 | base = non_root_directories.sample 336 | setup_expectation(base, source_file_name) 337 | file_list.source_file(source_file_name, from: base) 338 | end 339 | end 340 | 341 | specify '#default_search_pathでfrom:未指定時の挙動を変更できる' do 342 | file_list = described_class.new(context, path) 343 | 344 | setup_expectation(root_directories[0], source_file_name) 345 | file_list.default_search_path(source_file: :root) 346 | file_list.source_file(source_file_name) 347 | 348 | setup_expectation(root_directories.last, source_file_name) 349 | file_list.default_search_path(source_file: :local_root) 350 | file_list.source_file(source_file_name) 351 | 352 | setup_expectation(__dir__, source_file_name) 353 | file_list.default_search_path(source_file: :current) 354 | file_list.source_file(source_file_name) 355 | 356 | mock_cwd 357 | setup_expectation(__dir__, source_file_name) 358 | file_list.default_search_path(source_file: :cwd) 359 | file_list.source_file(source_file_name) 360 | 361 | base = non_root_directories.sample 362 | setup_expectation(base, source_file_name) 363 | file_list.default_search_path(source_file: base) 364 | file_list.source_file(source_file_name) 365 | 366 | setup_expectation(__dir__, source_file_name) 367 | file_list.reset_default_search_path(:source_file) 368 | file_list.source_file(source_file_name) 369 | end 370 | 371 | context '指定したソースファイルが存在しない場合' do 372 | it 'LoadErrorを起こす' do 373 | file_list = described_class.new(context, path) 374 | [*root_directories, __dir__].each do |root| 375 | allow(File).to receive(:file?).with(File.join(root, source_file_name)).and_return(false) 376 | end 377 | 378 | expect { 379 | file_list.source_file(source_file_name) 380 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{source_file_name} @#{__FILE__}:#{__LINE__ - 1}" 381 | 382 | expect { 383 | file_list.source_file(source_file_name, from: :root) 384 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{source_file_name} @#{__FILE__}:#{__LINE__ - 1}" 385 | end 386 | end 387 | 388 | context '指定したソースファイルが存在せず、raise_error:falseが指定されている場合' do 389 | it 'エラーを起こさない' do 390 | file_list = described_class.new(context, path) 391 | [*root_directories, __dir__].each do |root| 392 | allow(File).to receive(:file?).with(File.join(root, source_file_name)).and_return(false) 393 | end 394 | 395 | expect { 396 | file_list.source_file(source_file_name, raise_error: false) 397 | }.not_to raise_error 398 | 399 | expect { 400 | file_list.source_file(source_file_name, from: :root, raise_error: false) 401 | }.not_to raise_error 402 | end 403 | end 404 | end 405 | 406 | describe '#library_file' do 407 | let(:library_file_name) do 408 | 'piyo.sv' 409 | end 410 | 411 | def setup_expectation(root, file_name, *invalid_roots) 412 | path = 413 | if root.empty? 414 | file_name 415 | else 416 | File.join(root, file_name) 417 | end 418 | 419 | allow(File).to receive(:file?).with(path).and_return(true) 420 | expect(context).to receive(:add_library_file).with(path) 421 | 422 | invalid_roots.each do |invalid_root| 423 | allow(File).to receive(:file?).with(File.join(invalid_root, file_name)).and_return(false) 424 | end 425 | end 426 | 427 | it '呼び出し元を起点に、contextにライブラリファイルを追加する' do 428 | file_list = described_class.new(context, path) 429 | 430 | setup_expectation(__dir__, library_file_name) 431 | file_list.library_file(library_file_name) 432 | end 433 | 434 | context '絶対パスで指定された場合' do 435 | it '指定されたライブラリファイルをcontextにそのまま追加する' do 436 | file_list = described_class.new(context, '') 437 | file_path = File.join(__dir__, library_file_name) 438 | 439 | setup_expectation('', file_path) 440 | file_list.library_file(file_path) 441 | end 442 | end 443 | 444 | context 'from: :rootが指定された場合' do 445 | it 'ルートディレクトリ中を検索し、contextにライブラリファイルを追加する' do 446 | file_list = described_class.new(context, path) 447 | file_paths = root_directories.map { |root| File.join(root, library_file_name) } 448 | 449 | setup_expectation(root_directories[0], library_file_name) 450 | file_list.library_file(library_file_name, from: :root) 451 | 452 | setup_expectation(root_directories[1], library_file_name, root_directories[0]) 453 | file_list.library_file(library_file_name, from: :root) 454 | end 455 | end 456 | 457 | context 'from: :local_rootが指定された場合' do 458 | it '直近のリポジトリルートから検索を行う' do 459 | file_path = File.join(root_directories.last, library_file_name) 460 | file_list = described_class.new(context, path) 461 | 462 | setup_expectation(root_directories.last, library_file_name) 463 | file_list.library_file(library_file_name, from: :local_root) 464 | end 465 | end 466 | 467 | context 'from: currentが指定された場合' do 468 | it '呼び出し元を起点として、contextにライブラリファイルを追加する' do 469 | file_list = described_class.new(context, path) 470 | 471 | setup_expectation(__dir__, library_file_name) 472 | file_list.library_file(library_file_name, from: :current) 473 | end 474 | end 475 | 476 | context 'from: cwdが指定された場合' do 477 | it '実行ディレクトリを起点として、contextにライブラリファイルを追加する' do 478 | file_list = described_class.new(context, path) 479 | 480 | mock_cwd 481 | setup_expectation(__dir__, library_file_name) 482 | file_list.library_file(library_file_name, from: :cwd) 483 | end 484 | end 485 | 486 | context 'from:でベースディレクトリが指定された場合' do 487 | it '指定されたベースディレクトリから、contextにライブラリファイルを追加する' do 488 | file_list = described_class.new(context, path) 489 | 490 | base = non_root_directories.sample 491 | setup_expectation(base, library_file_name) 492 | file_list.library_file(library_file_name, from: base) 493 | end 494 | end 495 | 496 | specify '#default_search_pathでfrom:未指定時の挙動を変更できる' do 497 | file_list = described_class.new(context, path) 498 | 499 | setup_expectation(root_directories[0], library_file_name) 500 | file_list.default_search_path(library_file: :root) 501 | file_list.library_file(library_file_name) 502 | 503 | setup_expectation(root_directories.last, library_file_name) 504 | file_list.default_search_path(library_file: :local_root) 505 | file_list.library_file(library_file_name) 506 | 507 | setup_expectation(__dir__, library_file_name) 508 | file_list.default_search_path(library_file: :current) 509 | file_list.library_file(library_file_name) 510 | 511 | mock_cwd 512 | setup_expectation(__dir__, library_file_name) 513 | file_list.default_search_path(library_file: :cwd) 514 | file_list.library_file(library_file_name) 515 | 516 | base = non_root_directories.sample 517 | setup_expectation(base, library_file_name) 518 | file_list.default_search_path(library_file: base) 519 | file_list.library_file(library_file_name) 520 | 521 | setup_expectation(__dir__, library_file_name) 522 | file_list.reset_default_search_path(:library_file) 523 | file_list.library_file(library_file_name) 524 | end 525 | 526 | context '指定したライブラリファイルが存在しない場合' do 527 | it 'LoadErrorを起こす' do 528 | file_list = described_class.new(context, path) 529 | [*root_directories, __dir__].each do |root| 530 | allow(File).to receive(:file?).with(File.join(root, library_file_name)).and_return(false) 531 | end 532 | 533 | expect { 534 | file_list.library_file(library_file_name) 535 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{library_file_name} @#{__FILE__}:#{__LINE__ - 1}" 536 | 537 | expect { 538 | file_list.library_file(library_file_name, from: :root) 539 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{library_file_name} @#{__FILE__}:#{__LINE__ - 1}" 540 | end 541 | end 542 | 543 | context '指定したライブラリファイルが存在せず、raise_error:falseが指定されている場合' do 544 | it 'エラーを起こさない' do 545 | file_list = described_class.new(context, path) 546 | [*root_directories, __dir__].each do |root| 547 | allow(File).to receive(:file?).with(File.join(root, library_file_name)).and_return(false) 548 | end 549 | 550 | expect { 551 | file_list.library_file(library_file_name, raise_error: false) 552 | }.not_to raise_error 553 | 554 | expect { 555 | file_list.library_file(library_file_name, from: :root, raise_error: false) 556 | }.not_to raise_error 557 | end 558 | end 559 | end 560 | 561 | describe '#include_directory' do 562 | let(:include_directories) do 563 | ['foo', 'bar/baz'] 564 | end 565 | 566 | def setup_expectation(root, dir, valid = true) 567 | path = 568 | if root.empty? 569 | dir 570 | else 571 | File.join(root, dir) 572 | end 573 | 574 | allow(File).to receive(:directory?).with(path).and_return(valid) 575 | if valid 576 | expect(context).to receive(:add_include_directory).with(path) 577 | end 578 | end 579 | 580 | it '呼び出し元を起点に、指定されたディレクトリをcontextに追加する' do 581 | file_list = described_class.new(context, path) 582 | 583 | setup_expectation(__dir__, include_directories[0]) 584 | setup_expectation(__dir__, include_directories[1]) 585 | file_list.include_directory(include_directories[0]) 586 | file_list.include_directory(include_directories[1]) 587 | end 588 | 589 | context '絶対パスで指定された場合' do 590 | it '指定されたディレクトリを、そのままcontexに追加する' do 591 | file_list = described_class.new(context, path) 592 | directory_paths = include_directories.map { |dir| File.join('/', dir) } 593 | 594 | setup_expectation('', directory_paths[0]) 595 | setup_expectation('', directory_paths[1]) 596 | file_list.include_directory(directory_paths[0]) 597 | file_list.include_directory(directory_paths[1]) 598 | end 599 | end 600 | 601 | context 'from: :rootが指定された場合' do 602 | it 'ルートディレクトリ中を検索し、contextに指定されたディレクトリを追加する' do 603 | file_list = described_class.new(context, path) 604 | 605 | setup_expectation(root_directories[0], include_directories[0], true) 606 | setup_expectation(root_directories[0], include_directories[1], false) 607 | setup_expectation(root_directories[1], include_directories[1], true) 608 | file_list.include_directory(include_directories[0], from: :root) 609 | file_list.include_directory(include_directories[1], from: :root) 610 | end 611 | end 612 | 613 | context 'from: :local_rootが指定された場合' do 614 | it '直近のリポジトリルートから検索を行う' do 615 | file_list = described_class.new(context, path) 616 | 617 | setup_expectation(root_directories.last, include_directories[0]) 618 | setup_expectation(root_directories.last, include_directories[1]) 619 | file_list.include_directory(include_directories[0], from: :local_root) 620 | file_list.include_directory(include_directories[1], from: :local_root) 621 | end 622 | end 623 | 624 | context 'from: :currentが指定された場合' do 625 | it '呼び出し元を起点に、指定されたディレクトリをcontextに追加する' do 626 | file_list = described_class.new(context, path) 627 | 628 | setup_expectation(__dir__, include_directories[0]) 629 | setup_expectation(__dir__, include_directories[1]) 630 | file_list.include_directory(include_directories[0], from: :current) 631 | file_list.include_directory(include_directories[1], from: :current) 632 | end 633 | end 634 | 635 | context 'from: :cwdが指定された場合' do 636 | it '実行ディレクトリを起点に、指定されたディレクトリをcontextに追加する' do 637 | file_list = described_class.new(context, path) 638 | 639 | mock_cwd 640 | setup_expectation(__dir__, include_directories[0]) 641 | setup_expectation(__dir__, include_directories[1]) 642 | file_list.include_directory(include_directories[0], from: :cwd) 643 | file_list.include_directory(include_directories[1], from: :cwd) 644 | end 645 | end 646 | 647 | context 'from:でベースディレクトリが指定された場合' do 648 | it 'ベースディレクトリから、指定されたディレクトリをcontextに追加する' do 649 | file_list = described_class.new(context, path) 650 | base = non_root_directories.sample 651 | 652 | setup_expectation(base, include_directories[0]) 653 | setup_expectation(base, include_directories[1]) 654 | file_list.include_directory(include_directories[0], from: base) 655 | file_list.include_directory(include_directories[1], from: base) 656 | end 657 | end 658 | 659 | specify '#default_search_pathでfrom:未指定時の挙動を変更できる' do 660 | file_list = described_class.new(context, path) 661 | 662 | setup_expectation(root_directories[0], include_directories[0]) 663 | file_list.default_search_path(include_directory: :root) 664 | file_list.include_directory(include_directories[0]) 665 | 666 | setup_expectation(root_directories.last, include_directories[0]) 667 | file_list.default_search_path(include_directory: :local_root) 668 | file_list.include_directory(include_directories[0]) 669 | 670 | setup_expectation(__dir__, include_directories[0]) 671 | file_list.default_search_path(include_directory: :current) 672 | file_list.include_directory(include_directories[0]) 673 | 674 | mock_cwd 675 | setup_expectation(__dir__, include_directories[0]) 676 | file_list.default_search_path(include_directory: :cwd) 677 | file_list.include_directory(include_directories[0]) 678 | 679 | base = non_root_directories.sample 680 | setup_expectation(base, include_directories[0]) 681 | file_list.default_search_path(include_directory: base) 682 | file_list.include_directory(include_directories[0]) 683 | 684 | setup_expectation(__dir__, include_directories[0]) 685 | file_list.reset_default_search_path(:include_directory) 686 | file_list.include_directory(include_directories[0]) 687 | end 688 | 689 | context '指定したディレクトリが存在しない場合' do 690 | it 'NoEntryErrorを起こす' do 691 | file_list = described_class.new(context, path) 692 | [*root_directories, __dir__].each do |dir| 693 | allow(File).to receive(:directory?).with(File.join(dir, include_directories[0])).and_return(false) 694 | end 695 | 696 | expect { 697 | file_list.include_directory(include_directories[0]) 698 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{include_directories[0]} @#{__FILE__}:#{__LINE__ - 1}" 699 | 700 | expect { 701 | file_list.include_directory(include_directories[0], from: :root) 702 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{include_directories[0]} @#{__FILE__}:#{__LINE__ - 1}" 703 | end 704 | end 705 | 706 | context '指定したディレクトリが存在せず、raise_error:falseが指定されている場合' do 707 | it 'エラーを起こさない' do 708 | file_list = described_class.new(context, path) 709 | [*root_directories, __dir__].each do |dir| 710 | allow(File).to receive(:directory?).with(File.join(dir, include_directories[0])).and_return(false) 711 | end 712 | 713 | expect { 714 | file_list.include_directory(include_directories[0], raise_error: false) 715 | }.not_to raise_error 716 | 717 | expect { 718 | file_list.include_directory(include_directories[0], from: :root, raise_error: false) 719 | }.not_to raise_error 720 | end 721 | end 722 | end 723 | 724 | describe '#library_directory' do 725 | let(:library_directories) do 726 | ['foo', 'bar/baz'] 727 | end 728 | 729 | def setup_expectation(root, dir, valid = true) 730 | path = 731 | if root.empty? 732 | dir 733 | else 734 | File.join(root, dir) 735 | end 736 | 737 | allow(File).to receive(:directory?).with(path).and_return(valid) 738 | if valid 739 | expect(context).to receive(:add_library_directory).with(path) 740 | end 741 | end 742 | 743 | it '呼び出し元を起点に、指定されたディレクトリをcontextに追加する' do 744 | file_list = described_class.new(context, path) 745 | 746 | setup_expectation(__dir__, library_directories[0]) 747 | setup_expectation(__dir__, library_directories[1]) 748 | file_list.library_directory(library_directories[0]) 749 | file_list.library_directory(library_directories[1]) 750 | end 751 | 752 | context '絶対パスで指定された場合' do 753 | it '指定されたディレクトリを、そのままcontexに追加する' do 754 | file_list = described_class.new(context, path) 755 | directory_paths = library_directories.map { |dir| File.join('/', dir) } 756 | 757 | setup_expectation('', directory_paths[0]) 758 | setup_expectation('', directory_paths[1]) 759 | file_list.library_directory(directory_paths[0]) 760 | file_list.library_directory(directory_paths[1]) 761 | end 762 | end 763 | 764 | context 'from: :rootが指定された場合' do 765 | it 'ルートディレクトリ中を検索し、contextに指定されたディレクトリを追加する' do 766 | file_list = described_class.new(context, path) 767 | 768 | setup_expectation(root_directories[0], library_directories[0], true) 769 | setup_expectation(root_directories[0], library_directories[1], false) 770 | setup_expectation(root_directories[1], library_directories[1], true) 771 | file_list.library_directory(library_directories[0], from: :root) 772 | file_list.library_directory(library_directories[1], from: :root) 773 | end 774 | end 775 | 776 | context 'from: :local_rootが指定された場合' do 777 | it '直近のリポジトリルートから検索を行う' do 778 | file_list = described_class.new(context, path) 779 | 780 | setup_expectation(root_directories.last, library_directories[0]) 781 | setup_expectation(root_directories.last, library_directories[1]) 782 | file_list.library_directory(library_directories[0], from: :local_root) 783 | file_list.library_directory(library_directories[1], from: :local_root) 784 | end 785 | end 786 | 787 | context 'from: :currentが指定された場合' do 788 | it '呼び出し元を起点に、指定されたディレクトリをcontextに追加する' do 789 | file_list = described_class.new(context, path) 790 | 791 | setup_expectation(__dir__, library_directories[0]) 792 | setup_expectation(__dir__, library_directories[1]) 793 | file_list.library_directory(library_directories[0], from: :current) 794 | file_list.library_directory(library_directories[1], from: :current) 795 | end 796 | end 797 | 798 | context 'from: :cwdが指定された場合' do 799 | it '実行ディレクトリ起点に、指定されたディレクトリをcontextに追加する' do 800 | file_list = described_class.new(context, path) 801 | 802 | mock_cwd 803 | setup_expectation(__dir__, library_directories[0]) 804 | setup_expectation(__dir__, library_directories[1]) 805 | file_list.library_directory(library_directories[0], from: :cwd) 806 | file_list.library_directory(library_directories[1], from: :cwd) 807 | end 808 | end 809 | 810 | context 'from:でベースディレクトリが指定された場合' do 811 | it 'ベースディレクトリから、指定されたディレクトリをcontextに追加する' do 812 | file_list = described_class.new(context, path) 813 | base = non_root_directories.sample 814 | 815 | setup_expectation(base, library_directories[0]) 816 | setup_expectation(base, library_directories[1]) 817 | file_list.library_directory(library_directories[0], from: base) 818 | file_list.library_directory(library_directories[1], from: base) 819 | end 820 | end 821 | 822 | specify '#default_search_pathでfrom:未指定時の挙動を変更できる' do 823 | file_list = described_class.new(context, path) 824 | 825 | setup_expectation(root_directories[0], library_directories[0]) 826 | file_list.default_search_path(library_directory: :root) 827 | file_list.library_directory(library_directories[0]) 828 | 829 | setup_expectation(root_directories.last, library_directories[0]) 830 | file_list.default_search_path(library_directory: :local_root) 831 | file_list.library_directory(library_directories[0]) 832 | 833 | setup_expectation(__dir__, library_directories[0]) 834 | file_list.default_search_path(library_directory: :current) 835 | file_list.library_directory(library_directories[0]) 836 | 837 | mock_cwd 838 | setup_expectation(__dir__, library_directories[0]) 839 | file_list.default_search_path(library_directory: :cwd) 840 | file_list.library_directory(library_directories[0]) 841 | 842 | base = non_root_directories.sample 843 | setup_expectation(base, library_directories[0]) 844 | file_list.default_search_path(library_directory: base) 845 | file_list.library_directory(library_directories[0]) 846 | 847 | setup_expectation(__dir__, library_directories[0]) 848 | file_list.reset_default_search_path(:library_directory) 849 | file_list.library_directory(library_directories[0]) 850 | end 851 | 852 | context '指定したディレクトリが存在しない場合' do 853 | it 'NoEntryErrorを起こす' do 854 | file_list = described_class.new(context, path) 855 | [*root_directories, __dir__].each do |dir| 856 | allow(File).to receive(:directory?).with(File.join(dir, library_directories[0])).and_return(false) 857 | end 858 | 859 | expect { 860 | file_list.library_directory(library_directories[0]) 861 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{library_directories[0]} @#{__FILE__}:#{__LINE__ - 1}" 862 | 863 | expect { 864 | file_list.library_directory(library_directories[0], from: :root) 865 | }.to raise_error FLGen::NoEntryError, "no such file or directory -- #{library_directories[0]} @#{__FILE__}:#{__LINE__ - 1}" 866 | end 867 | end 868 | 869 | context '指定したディレクトリが存在せず、raise_error:f alseが指定されている場合' do 870 | it 'エラーを起こさない' do 871 | file_list = described_class.new(context, path) 872 | [*root_directories, __dir__].each do |dir| 873 | allow(File).to receive(:directory?).with(File.join(dir, library_directories[0])).and_return(false) 874 | end 875 | 876 | expect { 877 | file_list.library_directory(library_directories[0], raise_error: false) 878 | }.not_to raise_error 879 | 880 | expect { 881 | file_list.library_directory(library_directories[0], from: :root, raise_error: false) 882 | }.not_to raise_error 883 | end 884 | end 885 | end 886 | 887 | describe '#find_files/#find_file' do 888 | def mock_glob(pattens, results) 889 | results.each do |(base, paths)| 890 | allow(Dir).to receive(:glob).with(match(Array(pattens)), base: base).and_return(Array(paths)) 891 | Array(paths).each do |path| 892 | allow(File).to receive(:file?).with(File.join(base, path)).and_return(true) 893 | end 894 | end 895 | end 896 | 897 | let(:file_names) do 898 | ['foo.sv', 'bar.sv', 'baz.v', 'qux.v'] 899 | end 900 | 901 | before do 902 | allow(Dir).to receive(:glob).with(any_args).and_call_original 903 | end 904 | 905 | it '呼び出し元を起点として、入力パターンに一致するファイル一覧を返す' do 906 | file_list = described_class.new(context, path) 907 | 908 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 909 | expect(file_list.find_file('*.sv')) 910 | .to eq File.join(__dir__, file_names[0]) 911 | expect(file_list.find_files('*.sv')) 912 | .to match([ 913 | File.join(__dir__, file_names[0]), 914 | File.join(__dir__, file_names[1]) 915 | ]) 916 | expect(file_list.find_file('*.v')).to be_nil 917 | expect(file_list.find_files('*.v')).to be_empty 918 | 919 | mock_glob(['*.sv', '*.v'], { __dir__ => file_names }) 920 | expect(file_list.find_file(['*.sv', '*.v'])) 921 | .to eq File.join(__dir__, file_names[0]) 922 | expect(file_list.find_files(['*.sv', '*.v'])) 923 | .to match([ 924 | File.join(__dir__, file_names[0]), 925 | File.join(__dir__, file_names[1]), 926 | File.join(__dir__, file_names[2]), 927 | File.join(__dir__, file_names[3]) 928 | ]) 929 | end 930 | 931 | context 'ブロックが与えられた場合' do 932 | it 'パターンに一致したパスを引数として、ブロックを実行する' do 933 | file_list = described_class.new(context, path) 934 | 935 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 936 | expect { |b| file_list.find_file('*.sv', &b) } 937 | .to yield_with_args(File.join(__dir__, file_names[0])) 938 | expect { |b| file_list.find_files('*.sv', &b) } 939 | .to yield_successive_args( 940 | File.join(__dir__, file_names[0]), 941 | File.join(__dir__, file_names[1]) 942 | ) 943 | expect { |b| file_list.find_file('*.v', &b) } 944 | .not_to yield_control 945 | expect { |b| file_list.find_files('*.v', &b) } 946 | .not_to yield_control 947 | 948 | mock_glob(['*.sv', '*.v'], { __dir__ => file_names }) 949 | expect { |b| file_list.find_file(['*.sv', '*.v'], &b) } 950 | .to yield_with_args(File.join(__dir__, file_names[0])) 951 | expect { |b| file_list.find_files(['*.sv', '*.v'], &b) } 952 | .to yield_successive_args( 953 | File.join(__dir__, file_names[0]), 954 | File.join(__dir__, file_names[1]), 955 | File.join(__dir__, file_names[2]), 956 | File.join(__dir__, file_names[3]) 957 | ) 958 | end 959 | end 960 | 961 | context 'from: :rootが指定された場合' do 962 | it 'ルートディレクトリを起点として、入力パターンに一致するファイル一覧を返す' do 963 | file_list = described_class.new(context, path) 964 | 965 | mock_glob('*.sv', { root_directories[0] => file_names[0], root_directories[1] => file_names[1] }) 966 | expect(file_list.find_file('*.sv', from: :root)) 967 | .to eq File.join(root_directories[0], file_names[0]) 968 | expect(file_list.find_files('*.sv', from: :root)) 969 | .to match([ 970 | File.join(root_directories[0], file_names[0]), 971 | File.join(root_directories[1], file_names[1]) 972 | ]) 973 | expect(file_list.find_file('*.v', from: :root)).to be_nil 974 | expect(file_list.find_files('*.v', from: :root)).to be_empty 975 | 976 | mock_glob(['*.sv', '*.v'], { root_directories[0] => [file_names[0], file_names[2]], root_directories[1] => [file_names[1], file_names[3]] }) 977 | expect(file_list.find_file(['*.sv', '*.v'], from: :root)) 978 | .to eq File.join(root_directories[0], file_names[0]) 979 | expect(file_list.find_files(['*.sv', '*.v'], from: :root)) 980 | .to match([ 981 | File.join(root_directories[0], file_names[0]), 982 | File.join(root_directories[0], file_names[2]), 983 | File.join(root_directories[1], file_names[1]), 984 | File.join(root_directories[1], file_names[3]) 985 | ]) 986 | end 987 | end 988 | 989 | context 'from: :local_rootが指定された場合' do 990 | it '直近のルートディレクトリを起点として、入力パターンに一致するファイル一覧を返す' do 991 | file_list = described_class.new(context, path) 992 | 993 | mock_glob('*.sv', { root_directories.last => file_names[0..1] }) 994 | expect(file_list.find_file('*.sv', from: :local_root)) 995 | .to eq File.join(root_directories.last, file_names[0]) 996 | expect(file_list.find_files('*.sv', from: :local_root)) 997 | .to match([ 998 | File.join(root_directories.last, file_names[0]), 999 | File.join(root_directories.last, file_names[1]) 1000 | ]) 1001 | expect(file_list.find_file('*.v', from: :local_root)).to be_nil 1002 | expect(file_list.find_files('*.v', from: :local_root)).to be_empty 1003 | 1004 | mock_glob(['*.sv', '*.v'], { root_directories.last => file_names }) 1005 | expect(file_list.find_file(['*.sv', '*.v'], from: :local_root)) 1006 | .to eq File.join(root_directories.last, file_names[0]) 1007 | expect(file_list.find_files(['*.sv', '*.v'], from: :local_root)) 1008 | .to match([ 1009 | File.join(root_directories.last, file_names[0]), 1010 | File.join(root_directories.last, file_names[1]), 1011 | File.join(root_directories.last, file_names[2]), 1012 | File.join(root_directories.last, file_names[3]) 1013 | ]) 1014 | end 1015 | end 1016 | 1017 | context 'from: :currentが指定された場合' do 1018 | it '呼び出し元を起点として、入力パターンに一致するファイル一覧を返す' do 1019 | file_list = described_class.new(context, path) 1020 | 1021 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 1022 | expect(file_list.find_file('*.sv', from: :current)) 1023 | .to eq File.join(__dir__, file_names[0]) 1024 | expect(file_list.find_files('*.sv', from: :current)) 1025 | .to match([ 1026 | File.join(__dir__, file_names[0]), 1027 | File.join(__dir__, file_names[1]) 1028 | ]) 1029 | expect(file_list.find_file('*.v', from: :current)).to be_nil 1030 | expect(file_list.find_files('*.v', from: :current)).to be_empty 1031 | 1032 | mock_glob(['*.sv', '*.v'] , { __dir__ => file_names }) 1033 | expect(file_list.find_file(['*.sv', '*.v'], from: :current)) 1034 | .to eq File.join(__dir__, file_names[0]) 1035 | expect(file_list.find_files(['*.sv', '*.v'], from: :current)) 1036 | .to match([ 1037 | File.join(__dir__, file_names[0]), 1038 | File.join(__dir__, file_names[1]), 1039 | File.join(__dir__, file_names[2]), 1040 | File.join(__dir__, file_names[3]) 1041 | ]) 1042 | end 1043 | end 1044 | 1045 | context 'from: :cwdが指定された場合' do 1046 | it '実行ディレクトリを起点として、入力パターンに一致するファイル一覧を返す' do 1047 | file_list = described_class.new(context, path) 1048 | 1049 | mock_cwd 1050 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 1051 | expect(file_list.find_file('*.sv', from: :cwd)) 1052 | .to eq File.join(__dir__, file_names[0]) 1053 | expect(file_list.find_files('*.sv', from: :cwd)) 1054 | .to match([ 1055 | File.join(__dir__, file_names[0]), 1056 | File.join(__dir__, file_names[1]) 1057 | ]) 1058 | expect(file_list.find_file('*.v', from: :cwd)).to be_nil 1059 | expect(file_list.find_files('*.v', from: :cwd)).to be_empty 1060 | 1061 | mock_cwd 1062 | mock_glob(['*.sv', '*.v'], { __dir__ => file_names }) 1063 | expect(file_list.find_file(['*.sv', '*.v'], from: :cwd)) 1064 | .to eq File.join(__dir__, file_names[0]) 1065 | expect(file_list.find_files(['*.sv', '*.v'], from: :cwd)) 1066 | .to match([ 1067 | File.join(__dir__, file_names[0]), 1068 | File.join(__dir__, file_names[1]), 1069 | File.join(__dir__, file_names[2]), 1070 | File.join(__dir__, file_names[3]) 1071 | ]) 1072 | end 1073 | end 1074 | 1075 | context 'from:でベースディレクトリが指定された場合' do 1076 | it 'ベースディレクトリを起点として、入力パターンに一致するファイル一覧を返す' do 1077 | file_list = described_class.new(context, path) 1078 | base = non_root_directories.sample 1079 | 1080 | mock_glob('*.sv', { base => file_names[0..1] }) 1081 | expect(file_list.find_file('*.sv', from: base)) 1082 | .to eq File.join(base, file_names[0]) 1083 | expect(file_list.find_files('*.sv', from: base)) 1084 | .to match([ 1085 | File.join(base, file_names[0]), 1086 | File.join(base, file_names[1]) 1087 | ]) 1088 | expect(file_list.find_file('*.v', from: base)).to be_nil 1089 | expect(file_list.find_files('*.v', from: base)).to be_empty 1090 | 1091 | mock_glob(['*.sv', '*.v'], { base => file_names }) 1092 | expect(file_list.find_file(['*.sv', '*.v'], from: base)) 1093 | .to eq File.join(base, file_names[0]) 1094 | expect(file_list.find_files(['*.sv', '*.v'], from: base)) 1095 | .to match([ 1096 | File.join(base, file_names[0]), 1097 | File.join(base, file_names[1]), 1098 | File.join(base, file_names[2]), 1099 | File.join(base, file_names[3]) 1100 | ]) 1101 | end 1102 | end 1103 | 1104 | specify '#default_search_pathでfrom:未指定時の挙動を変更できる' do 1105 | file_list = file_list = described_class.new(context, path) 1106 | 1107 | mock_glob('*.sv', { root_directories[0] => file_names[0], root_directories[1] => file_names[1] }) 1108 | file_list.default_search_path(find_files: :root, find_file: :root) 1109 | expect(file_list.find_file('*.sv')) 1110 | .to eq File.join(root_directories[0], file_names[0]) 1111 | expect(file_list.find_files('*.sv')) 1112 | .to match([ 1113 | File.join(root_directories[0], file_names[0]), 1114 | File.join(root_directories[1], file_names[1]) 1115 | ]) 1116 | 1117 | mock_glob('*.sv', { root_directories.last => file_names[0..1] }) 1118 | file_list.default_search_path(find_files: :local_root, find_file: :local_root) 1119 | expect(file_list.find_file('*.sv')) 1120 | .to eq File.join(root_directories.last, file_names[0]) 1121 | expect(file_list.find_files('*.sv')) 1122 | .to match([ 1123 | File.join(root_directories.last, file_names[0]), 1124 | File.join(root_directories.last, file_names[1]) 1125 | ]) 1126 | 1127 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 1128 | file_list.default_search_path(find_files: :current, find_file: :current) 1129 | expect(file_list.find_file('*.sv')) 1130 | .to eq File.join(__dir__, file_names[0]) 1131 | expect(file_list.find_files('*.sv')) 1132 | .to match([ 1133 | File.join(__dir__, file_names[0]), 1134 | File.join(__dir__, file_names[1]) 1135 | ]) 1136 | 1137 | mock_cwd 1138 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 1139 | file_list.default_search_path(find_files: :cwd, find_file: :cwd) 1140 | expect(file_list.find_file('*.sv')) 1141 | .to eq File.join(__dir__, file_names[0]) 1142 | expect(file_list.find_files('*.sv')) 1143 | .to match([ 1144 | File.join(__dir__, file_names[0]), 1145 | File.join(__dir__, file_names[1]) 1146 | ]) 1147 | 1148 | base = non_root_directories.sample 1149 | mock_glob('*.sv', { base => file_names[0..1] }) 1150 | file_list.default_search_path(find_files: base, find_file: base) 1151 | expect(file_list.find_file('*.sv')) 1152 | .to eq File.join(base, file_names[0]) 1153 | expect(file_list.find_files('*.sv')) 1154 | .to match([ 1155 | File.join(base, file_names[0]), 1156 | File.join(base, file_names[1]) 1157 | ]) 1158 | 1159 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 1160 | file_list.reset_default_search_path(:find_files, :find_file) 1161 | expect(file_list.find_file('*.sv')) 1162 | .to eq File.join(__dir__, file_names[0]) 1163 | expect(file_list.find_files('*.sv')) 1164 | .to match([ 1165 | File.join(__dir__, file_names[0]), 1166 | File.join(__dir__, file_names[1]) 1167 | ]) 1168 | end 1169 | 1170 | context 'ブロックを与えた場合' do 1171 | specify '一致したファイルを引数としてブロックを実行する' do 1172 | file_list = described_class.new(context, path) 1173 | 1174 | mock_glob('*.sv', { __dir__ => file_names[0..1] }) 1175 | expect { |b| 1176 | file_list.find_files('*.sv', &b) 1177 | }.to yield_successive_args(*file_names[0..1].map { |f| File.join(__dir__, f) }) 1178 | expect { |b| 1179 | file_list.find_files('.v', &b) 1180 | }.not_to yield_control 1181 | end 1182 | end 1183 | end 1184 | 1185 | describe '#file?' do 1186 | let(:file_names) do 1187 | ['foo.sv', 'bar.sv', 'baz.sv'] 1188 | end 1189 | 1190 | it '呼び出し元を起点として、指定されたファイルの有無を返す' do 1191 | file_list = described_class.new(context, path) 1192 | 1193 | allow(File).to receive(:file?).with(File.join(__dir__, file_names[0])).and_return(true) 1194 | expect(file_list.file?(file_names[0])).to eq true 1195 | 1196 | allow(File).to receive(:file?).with(File.join(__dir__, file_names[1])).and_return(false) 1197 | expect(file_list.file?(file_names[1])).to eq false 1198 | end 1199 | 1200 | context '絶対パスで指定された場合' do 1201 | it '指定されたファイルの有無を返す' do 1202 | file_list = described_class.new(context, path) 1203 | 1204 | path = File.join(__dir__, file_names[0]) 1205 | allow(File).to receive(:file?).with(path).and_return(true) 1206 | expect(file_list.file?(path)).to eq true 1207 | 1208 | path = File.join(__dir__, file_names[1]) 1209 | allow(File).to receive(:file?).with(path).and_return(false) 1210 | expect(file_list.file?(path)).to eq false 1211 | end 1212 | end 1213 | 1214 | context 'from: :rootが指定された場合' do 1215 | it 'ルートディレクトリを起点として、指定されたファイルの有無を返す' do 1216 | file_list = described_class.new(context, path) 1217 | 1218 | allow(File).to receive(:file?).with(File.join(root_directories[0], file_names[0])).and_return(true) 1219 | expect(file_list.file?(file_names[0], from: :root)).to eq true 1220 | 1221 | allow(File).to receive(:file?).with(File.join(root_directories[0], file_names[1])).and_return(false) 1222 | allow(File).to receive(:file?).with(File.join(root_directories[1], file_names[1])).and_return(true) 1223 | expect(file_list.file?(file_names[1], from: :root)).to eq true 1224 | 1225 | allow(File).to receive(:file?).with(File.join(root_directories[0], file_names[2])).and_return(false) 1226 | allow(File).to receive(:file?).with(File.join(root_directories[1], file_names[2])).and_return(false) 1227 | expect(file_list.file?(file_names[2], from: :root)).to eq false 1228 | end 1229 | end 1230 | 1231 | context 'from: :local_rootが指定された場合' do 1232 | it '直近のルートディレクトリを起点として、指定されたファイルの有無を返す' do 1233 | file_list = described_class.new(context, path) 1234 | 1235 | allow(File).to receive(:file?).with(File.join(root_directories.last, file_names[0])).and_return(true) 1236 | expect(file_list.file?(file_names[0], from: :local_root)).to eq true 1237 | 1238 | allow(File).to receive(:file?).with(File.join(root_directories.last, file_names[1])).and_return(false) 1239 | expect(file_list.file?(file_names[1], from: :local_root)).to eq false 1240 | end 1241 | end 1242 | 1243 | context 'from: :currentが指定された場合' do 1244 | it '呼び出し元を起点として、指定されたファイルの有無を返す' do 1245 | file_list = described_class.new(context, path) 1246 | 1247 | allow(File).to receive(:file?).with(File.join(__dir__, file_names[0])).and_return(true) 1248 | expect(file_list.file?(file_names[0], from: :current)).to eq true 1249 | 1250 | allow(File).to receive(:file?).with(File.join(__dir__, file_names[1])).and_return(false) 1251 | expect(file_list.file?(file_names[1], from: :current)).to eq false 1252 | end 1253 | end 1254 | 1255 | context 'from: :cwdが指定された場合' do 1256 | it '実行ディレクトリを起点として、指定されたファイルの有無を返す' do 1257 | file_list = described_class.new(context, path) 1258 | 1259 | mock_cwd 1260 | allow(File).to receive(:file?).with(File.join(__dir__, file_names[0])).and_return(true) 1261 | expect(file_list.file?(file_names[0], from: :cwd)).to eq true 1262 | 1263 | mock_cwd 1264 | allow(File).to receive(:file?).with(File.join(__dir__, file_names[1])).and_return(false) 1265 | expect(file_list.file?(file_names[1], from: :cwd)).to eq false 1266 | end 1267 | end 1268 | 1269 | context 'from:でベースディレクトリが指定された場合' do 1270 | it 'ベースディレクトリを起点として、指定されたファイルの有無を返す' do 1271 | file_list = described_class.new(context, path) 1272 | base = non_root_directories.sample 1273 | 1274 | allow(File).to receive(:file?).with(File.join(base, file_names[0])).and_return(true) 1275 | expect(file_list.file?(file_names[0], from: base)).to eq true 1276 | 1277 | allow(File).to receive(:file?).with(File.join(base, file_names[1])).and_return(false) 1278 | expect(file_list.file?(file_names[1], from: base)).to eq false 1279 | end 1280 | end 1281 | end 1282 | 1283 | describe '#directory?' do 1284 | let(:directory_names) do 1285 | ['foo', 'bar/baz', 'qux'] 1286 | end 1287 | 1288 | it '呼び出し元を起点として、指定されたディレクトリの有無を返す' do 1289 | file_list = described_class.new(context, path) 1290 | 1291 | allow(File).to receive(:directory?).with(File.join(__dir__, directory_names[0])).and_return(true) 1292 | expect(file_list.directory?(directory_names[0])).to eq true 1293 | 1294 | allow(File).to receive(:directory?).with(File.join(__dir__, directory_names[1])).and_return(false) 1295 | expect(file_list.directory?(directory_names[1])).to eq false 1296 | end 1297 | 1298 | context '絶対パスで指定された場合' do 1299 | it '指定されたディレクトリ有無を返す' do 1300 | file_list = described_class.new(context, path) 1301 | 1302 | path = File.join(__dir__, directory_names[0]) 1303 | allow(File).to receive(:directory?).with(path).and_return(true) 1304 | expect(file_list.directory?(path)).to eq true 1305 | 1306 | path = File.join(__dir__, directory_names[1]) 1307 | allow(File).to receive(:directory?).with(path).and_return(false) 1308 | expect(file_list.directory?(path)).to eq false 1309 | end 1310 | end 1311 | 1312 | context 'from: :rootが指定された場合' do 1313 | it 'ルートディレクトリを起点として、指定されたディレクトリの有無を返す' do 1314 | file_list = described_class.new(context, path) 1315 | 1316 | allow(File).to receive(:directory?).with(File.join(root_directories[0], directory_names[0])).and_return(true) 1317 | expect(file_list.directory?(directory_names[0], from: :root)).to eq true 1318 | 1319 | allow(File).to receive(:directory?).with(File.join(root_directories[0], directory_names[1])).and_return(false) 1320 | allow(File).to receive(:directory?).with(File.join(root_directories[1], directory_names[1])).and_return(true) 1321 | expect(file_list.directory?(directory_names[1], from: :root)).to eq true 1322 | 1323 | allow(File).to receive(:directory?).with(File.join(root_directories[0], directory_names[2])).and_return(false) 1324 | allow(File).to receive(:directory?).with(File.join(root_directories[1], directory_names[2])).and_return(false) 1325 | expect(file_list.directory?(directory_names[2], from: :root)).to eq false 1326 | end 1327 | end 1328 | 1329 | context 'from: :local_rootが指定された場合' do 1330 | it '直近のルートディレクトリを起点として、指定されたディレクトリの有無を返す' do 1331 | file_list = described_class.new(context, path) 1332 | 1333 | allow(File).to receive(:directory?).with(File.join(root_directories.last, directory_names[0])).and_return(true) 1334 | expect(file_list.directory?(directory_names[0], from: :local_root)).to eq true 1335 | 1336 | allow(File).to receive(:directory?).with(File.join(root_directories.last, directory_names[1])).and_return(false) 1337 | expect(file_list.directory?(directory_names[1], from: :local_root)).to eq false 1338 | end 1339 | end 1340 | 1341 | context 'from: :currentが指定された場合' do 1342 | it '呼び出し元を起点として、指定されたディレクトリの有無を返す' do 1343 | file_list = described_class.new(context, path) 1344 | 1345 | allow(File).to receive(:directory?).with(File.join(__dir__, directory_names[0])).and_return(true) 1346 | expect(file_list.directory?(directory_names[0], from: :current)).to eq true 1347 | 1348 | allow(File).to receive(:directory?).with(File.join(__dir__, directory_names[1])).and_return(false) 1349 | expect(file_list.directory?(directory_names[1], from: :current)).to eq false 1350 | end 1351 | end 1352 | 1353 | context 'from: :cwdが指定された場合' do 1354 | it '実行ディレクトリを起点として、指定されたディレクトリの有無を返す' do 1355 | file_list = described_class.new(context, path) 1356 | 1357 | mock_cwd 1358 | allow(File).to receive(:directory?).with(File.join(__dir__, directory_names[0])).and_return(true) 1359 | expect(file_list.directory?(directory_names[0], from: :cwd)).to eq true 1360 | 1361 | mock_cwd 1362 | allow(File).to receive(:directory?).with(File.join(__dir__, directory_names[1])).and_return(false) 1363 | expect(file_list.directory?(directory_names[1], from: :cwd)).to eq false 1364 | end 1365 | end 1366 | 1367 | context 'fromでベースディレクトリが指定された場合' do 1368 | it 'ベースディレクトリを起点として、指定されたディレクトリの有無を返す' do 1369 | file_list = described_class.new(context, path) 1370 | base = non_root_directories.sample 1371 | 1372 | allow(File).to receive(:directory?).with(File.join(base, directory_names[0])).and_return(true) 1373 | expect(file_list.directory?(directory_names[0], from: base)).to eq true 1374 | 1375 | allow(File).to receive(:directory?).with(File.join(base, directory_names[1])).and_return(false) 1376 | expect(file_list.directory?(directory_names[1], from: base)).to eq false 1377 | end 1378 | end 1379 | end 1380 | 1381 | describe '#define_macro' do 1382 | it 'マクロを定義する' do 1383 | file_list = described_class.new(context, path) 1384 | 1385 | expect(context).to receive(:define_macro).with(:FOO, nil) 1386 | expect(context).to receive(:define_macro).with(:BAR, 1) 1387 | 1388 | file_list.define_macro(:FOO) 1389 | file_list.define_macro(:BAR, 1) 1390 | end 1391 | end 1392 | 1393 | describe '#undefine_macro' do 1394 | it 'マクロを無効化する' do 1395 | file_list = described_class.new(context, path) 1396 | 1397 | expect(context).to receive(:undefine_macro).with(:FOO) 1398 | expect(context).to receive(:undefine_macro).with(:BAR) 1399 | 1400 | file_list.undefine_macro(:FOO) 1401 | file_list.undefine_macro(:BAR) 1402 | end 1403 | end 1404 | 1405 | describe '#macro?/#macro_defined?' do 1406 | let(:macros) do 1407 | [:FOO, :BAR] 1408 | end 1409 | 1410 | it '定義済みマクロかどうかを示す' do 1411 | file_list = described_class.new(context, path) 1412 | context.macros[macros[0]] = true 1413 | context.macros[macros[1]] = true 1414 | 1415 | expect(file_list.macro?(:FOO)).to be true 1416 | expect(file_list.macro?('FOO')).to be true 1417 | expect(file_list.macro?(:BAR)).to be true 1418 | expect(file_list.macro?('BAR')).to be true 1419 | expect(file_list.macro?(:BAZ)).to be false 1420 | expect(file_list.macro?('BAZ')).to be false 1421 | 1422 | expect(file_list.macro_defined?(:FOO)).to be true 1423 | expect(file_list.macro_defined?('FOO')).to be true 1424 | expect(file_list.macro_defined?(:BAR)).to be true 1425 | expect(file_list.macro_defined?('BAR')).to be true 1426 | expect(file_list.macro_defined?(:BAZ)).to be false 1427 | expect(file_list.macro_defined?('BAZ')).to be false 1428 | end 1429 | end 1430 | 1431 | describe '#macro' do 1432 | it '指定したマクロの値を返す' do 1433 | file_list = described_class.new(context, path) 1434 | context.macros[:FOO] = 'FOO' 1435 | context.macros[:BAR] = true 1436 | 1437 | expect(file_list.macro(:FOO)).to eq 'FOO' 1438 | expect(file_list.macro('FOO')).to eq 'FOO' 1439 | expect(file_list.macro(:BAR)).to be true 1440 | expect(file_list.macro('BAR')).to be true 1441 | end 1442 | 1443 | context '指定したマクロが未定義の場合' do 1444 | it 'nilを返す' do 1445 | file_list = described_class.new(context, path) 1446 | 1447 | expect(file_list.macro(:FOO)).to be_nil 1448 | expect(file_list.macro('FOO')).to be_nil 1449 | expect(file_list.macro(:BAR)).to be_nil 1450 | expect(file_list.macro('BAR')).to be_nil 1451 | end 1452 | end 1453 | end 1454 | 1455 | describe '#env?' do 1456 | it '指定された環境変数が定義されているかを返す' do 1457 | file_list = described_class.new(context, path) 1458 | 1459 | expect(file_list.env?('FOO')).to eq false 1460 | expect(file_list.env?(:FOO)).to eq false 1461 | 1462 | ENV['FOO'] = 'FOO' 1463 | expect(file_list.env?('FOO')).to eq true 1464 | expect(file_list.env?(:FOO)).to eq true 1465 | end 1466 | end 1467 | 1468 | describe '#env' do 1469 | it '指定された環境変数の値を取り出す' do 1470 | file_list = described_class.new(context, path) 1471 | ENV['FOO'] = 'FOO' 1472 | 1473 | expect(file_list.env('FOO')).to eq 'FOO' 1474 | expect(file_list.env(:FOO)).to eq 'FOO' 1475 | 1476 | expect(file_list.env('BAR')).to be_nil 1477 | expect(file_list.env(:BAR)).to be_nil 1478 | end 1479 | end 1480 | 1481 | describe '#target_tool?' do 1482 | context '対象ツールの指定がない場合' do 1483 | it 'falseを返す' do 1484 | file_list = described_class.new(context, path) 1485 | expect(file_list.target_tool?(:vcs)).to be false 1486 | expect(file_list.target_tool?(:xcelium)).to be false 1487 | end 1488 | end 1489 | 1490 | context '対象ツールの指定がある場合' do 1491 | it '比較結果を返す' do 1492 | file_list = described_class.new(context, path) 1493 | 1494 | context.options[:tool] = :vcs 1495 | expect(file_list.target_tool?(:vcs)).to be true 1496 | expect(file_list.target_tool?(:xcelium)).to be false 1497 | 1498 | context.options[:tool] = :xcelium 1499 | expect(file_list.target_tool?(:vcs)).to be false 1500 | expect(file_list.target_tool?(:xcelium)).to be true 1501 | end 1502 | end 1503 | end 1504 | 1505 | describe '#compile_argument' do 1506 | it 'contextにコンパイル引数を追加する' do 1507 | file_list = described_class.new(context, path) 1508 | 1509 | expect(context).to receive(:add_compile_argument).with(have_attributes(type: :generic, argument: '-foo', tool: nil)) 1510 | expect(context).to receive(:add_compile_argument).with(have_attributes(type: :generic, argument: '-bar=1', tool: nil)) 1511 | expect(context).to receive(:add_compile_argument).with(have_attributes(type: :generic, argument: '-fizz', tool: :vcs)) 1512 | expect(context).to receive(:add_compile_argument).with(have_attributes(type: :generic, argument: '-bazz=2', tool: :xcelium)) 1513 | 1514 | file_list.compile_argument('-foo') 1515 | file_list.compile_argument('-bar=1') 1516 | file_list.compile_argument('-fizz', tool: :vcs) 1517 | file_list.compile_argument('-bazz=2', tool: :xcelium) 1518 | end 1519 | end 1520 | 1521 | describe '#runtime_argument' do 1522 | it 'contextに実行時引数を追加する' do 1523 | file_list = described_class.new(context, path) 1524 | 1525 | expect(context).to receive(:add_runtime_argument).with(have_attributes(type: :generic, argument: '-foo', tool: nil)) 1526 | expect(context).to receive(:add_runtime_argument).with(have_attributes(type: :generic, argument: '-bar=1', tool: nil)) 1527 | expect(context).to receive(:add_runtime_argument).with(have_attributes(type: :generic, argument: '-fizz', tool: :vcs)) 1528 | expect(context).to receive(:add_runtime_argument).with(have_attributes(type: :generic, argument: '-bazz=2', tool: :xcelium)) 1529 | 1530 | file_list.runtime_argument('-foo') 1531 | file_list.runtime_argument('-bar=1') 1532 | file_list.runtime_argument('-fizz', tool: :vcs) 1533 | file_list.runtime_argument('-bazz=2', tool: :xcelium) 1534 | end 1535 | end 1536 | end 1537 | -------------------------------------------------------------------------------- /spec/flgen/source_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe FLGen::SourceFile do 4 | let(:root_directory) do 5 | '/fizz/buzz' 6 | end 7 | 8 | let(:paths) do 9 | ['foo/bar.sv', 'foo/bar.sv.gz'] 10 | end 11 | 12 | let(:path) do 13 | paths[0] 14 | end 15 | 16 | let(:file_content) do 17 | <<~'CODE' 18 | module foo; 19 | endmodule 20 | CODE 21 | end 22 | 23 | def create_soruce_file(root, path, checksum = nil) 24 | if checksum 25 | described_class.new(File.join(root, path), checksum) 26 | else 27 | described_class.new(File.join(root, path)) 28 | end 29 | end 30 | 31 | def match_path(*path) 32 | if path.size == 1 && path[0].is_a?(described_class) 33 | eq(path[0].path) 34 | else 35 | eq(File.join(*path)) 36 | end 37 | end 38 | 39 | describe '#path' do 40 | it '指定されたルートからのソースファイルのパスを返す' do 41 | source_file = create_soruce_file(root_directory, path) 42 | expect(source_file.path).to match_path(root_directory, path) 43 | end 44 | end 45 | 46 | describe '#==' do 47 | let(:file_contents) do 48 | contents = [] 49 | contents << <<~'CODE' 50 | module foo; 51 | endmodule 52 | CODE 53 | contents << <<~'CODE' 54 | module bar; 55 | endmodule 56 | CODE 57 | contents 58 | end 59 | 60 | context '右辺がSourceFileオブジェクトの場合' do 61 | it '#pathまたは#checksumが一致するかを返す' do 62 | lhs = create_soruce_file(root_directory, path) 63 | rhs = create_soruce_file(root_directory, path) 64 | expect(lhs).to eq(rhs) 65 | 66 | lhs = create_soruce_file(root_directory, path) 67 | rhs = create_soruce_file('/', path) 68 | allow(File).to receive(:read).with(lhs.path).and_return(file_contents[0]) 69 | allow(File).to receive(:read).with(rhs.path).and_return(file_contents[0]) 70 | expect(lhs).to eq(rhs) 71 | 72 | lhs = create_soruce_file(root_directory, paths[0]) 73 | rhs = create_soruce_file(root_directory, paths[1]) 74 | allow(File).to receive(:read).with(lhs.path).and_return(file_contents[0]) 75 | allow(File).to receive(:read).with(rhs.path).and_return(file_contents[1]) 76 | expect(lhs).not_to eq(rhs) 77 | end 78 | end 79 | 80 | context '右辺が文字列の場合' do 81 | it '#pathと一致するかを返す' do 82 | lhs = create_soruce_file(root_directory, path) 83 | rhs = File.join(root_directory, path) 84 | expect(lhs).to eq(rhs) 85 | 86 | lhs = create_soruce_file(root_directory, paths[0]) 87 | rhs = File.join(root_directory, paths[1]) 88 | expect(lhs).not_to eq(rhs) 89 | end 90 | end 91 | end 92 | 93 | describe '#match_ext?' do 94 | let(:source_file) do 95 | create_soruce_file(root_directory, path) 96 | end 97 | 98 | context '検索対象の拡張子が未指定の場合' do 99 | it 'falseを返す' do 100 | expect(source_file.match_ext?(nil)).to be false 101 | expect(source_file.match_ext?([])).to be false 102 | end 103 | end 104 | 105 | context 'ソースファイルの拡張子が、指定された拡張子中に存在する場合' do 106 | it 'trueを返す' do 107 | expect(source_file.match_ext?(['sv'])).to be true 108 | expect(source_file.match_ext?(['.sv'])).to be true 109 | expect(source_file.match_ext?(['.v', 'sv'])).to be true 110 | expect(source_file.match_ext?(['v', '.sv'])).to be true 111 | end 112 | end 113 | 114 | context 'ソースファイルの拡張子が、指定された拡張子中に存在しない場合' do 115 | it 'falseを返す' do 116 | expect(source_file.match_ext?(['v'])).to be false 117 | expect(source_file.match_ext?(['.v'])).to be false 118 | expect(source_file.match_ext?(['.c', 'v'])).to be false 119 | expect(source_file.match_ext?(['c', '.v'])).to be false 120 | end 121 | end 122 | end 123 | 124 | describe '#remove_ext' do 125 | let(:source_file) do 126 | create_soruce_file(root_directory, paths[1]) 127 | end 128 | 129 | context '削除対象の拡張子が未指定の場合' do 130 | it '元のパスを返す' do 131 | expect(source_file.remove_ext(nil).path).to match_path(source_file) 132 | expect(source_file.remove_ext([]).path).to match_path(source_file) 133 | end 134 | end 135 | 136 | context 'ソースファイルの拡張子が、削除対象の拡張子に含まれる場合' do 137 | before do 138 | allow(File).to receive(:read).with(source_file.path).and_return(file_content) 139 | end 140 | 141 | it '当該拡張子を削除したパスを返す' do 142 | expect(source_file.remove_ext(['gz']).path).to match_path(root_directory, path) 143 | expect(source_file.remove_ext(['.gz']).path).to match_path(root_directory, path) 144 | expect(source_file.remove_ext(['.bz2', 'gz']).path).to match_path(root_directory, path) 145 | expect(source_file.remove_ext(['bz2', '.gz']).path).to match_path(root_directory, path) 146 | end 147 | 148 | specify 'チェックサムは元のファイルのチェックサムを返す' do 149 | expect(source_file.remove_ext(['gz']).checksum) == source_file.checksum 150 | end 151 | end 152 | 153 | context 'ソースファイルの拡張子が、削除対象の拡張子に含まれない場合' do 154 | it '元のパスを返す' do 155 | expect(source_file.remove_ext(['bz2']).path).to match_path(source_file) 156 | expect(source_file.remove_ext(['.bz2']).path).to match_path(source_file) 157 | expect(source_file.remove_ext(['.zip', 'bz2']).path).to match_path(source_file) 158 | expect(source_file.remove_ext(['zip', '.bz2']).path).to match_path(source_file) 159 | end 160 | end 161 | end 162 | 163 | describe '#checksum' do 164 | let(:checksum) do 165 | Digest::MD5.digest(file_content) 166 | end 167 | 168 | it 'ソースファイルのチェックサムを求める' do 169 | source_file = create_soruce_file(root_directory, path) 170 | allow(File).to receive(:read).with(match_path(source_file)).and_return(file_content) 171 | expect(source_file.checksum).to eq checksum 172 | end 173 | 174 | context '生成時にチェックサムの指定がある場合' do 175 | it '指定されたチェックサムを返す' do 176 | source_file = create_soruce_file(root_directory, paths[1], checksum) 177 | expect(source_file.checksum).to eq checksum 178 | end 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'stringio' 5 | 6 | RSpec.configure do |config| 7 | # Enable flags like --only-failures and --next-failure 8 | config.example_status_persistence_file_path = '.rspec_status' 9 | 10 | # Disable RSpec exposing methods globally on `Module` and `main` 11 | config.disable_monkey_patching! 12 | 13 | config.expect_with :rspec do |c| 14 | c.syntax = :expect 15 | end 16 | 17 | if ENV.key?('COVERAGE') 18 | require 'simplecov' 19 | SimpleCov.start 20 | 21 | if ENV.key?('CI') 22 | require 'simplecov-cobertura' 23 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 24 | end 25 | end 26 | end 27 | 28 | require 'flgen' 29 | FLGEN_ROOT = File.expand_path('..', __dir__) 30 | --------------------------------------------------------------------------------