├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ └── feature-request.yml
├── codecov.yml
└── workflows
│ ├── test-and-release.yaml
│ └── test-job.yaml
├── .gitignore
├── LICENSE-Apache-2.0
├── LICENSE-MPL-2.0
├── README.md
├── doc
├── _head.html
├── horus-favicon.svg
├── horus-logo.svg
├── horus-social-preview.png
├── horus-social-preview.svg
├── overview.edoc
└── stylesheet.css
├── include
└── horus.hrl
├── mix.exs
├── priv
└── horus_cover_helper.erl
├── rebar.config
├── rebar.lock
├── src
├── horus.app.src
├── horus.erl
├── horus_cover.erl
├── horus_cover.hrl
├── horus_error.hrl
├── horus_fun.hrl
└── horus_utils.erl
└── test
├── arbitrary_mod.erl
├── cached_extractions.erl
├── cover_compile.erl
├── cover_compiled_mod1.erl
├── cover_compiled_mod2.erl
├── erlang_binaries.erl
├── erlang_blocks.erl
├── erlang_builtins.erl
├── erlang_exprs.erl
├── erlang_lists.erl
├── erlang_literals.erl
├── erlang_maps.erl
├── erlang_records.erl
├── erlang_tuples.erl
├── failing_funs.erl
├── fun_env.erl
├── fun_extraction_SUITE.erl
├── helpers.erl
├── helpers.hrl
├── is_module_loaded.erl
├── line_chunk.erl
├── local_vs_external.erl
├── macros.erl
├── misuses.erl
├── module_info.erl
├── nested_funs.erl
├── to_fun.erl
└── using_erl_eval.erl
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a report to help us improve
3 | labels: bug
4 |
5 | body:
6 | - type: textarea
7 | id: describe-problem
8 | attributes:
9 | label: What does not work?
10 | validations:
11 | required: true
12 |
13 | - type: textarea
14 | id: expected-behavior
15 | attributes:
16 | label: Expected behavior
17 | validations:
18 | required: true
19 |
20 | - type: textarea
21 | id: reproduction-steps
22 | attributes:
23 | label: How to reproduce
24 | validations:
25 | required: false
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea for this project
3 | labels: enhancement
4 |
5 | body:
6 | - type: textarea
7 | id: describe-problem
8 | attributes:
9 | label: Why
10 | validations:
11 | required: true
12 |
13 | - type: textarea
14 | id: describe-solution
15 | attributes:
16 | label: How
17 | validations:
18 | required: true
19 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | # The whole project must reach a coverage of 80% for the `codecov/project`
4 | # check to succeed.
5 | project:
6 | default:
7 | target: 80%
8 | threshold: 5% # How much the coverage can decrease.
9 | paths:
10 | - "!.github/"
11 |
12 | # The patch itself (i.e. the modified lines) must be 80% covered by tests
13 | # for the `codecov/patch` check to succeed.
14 | patch:
15 | default:
16 | target: 80%
17 | threshold: 5%
18 | paths:
19 | - "!.github/"
20 | informational: true
21 |
--------------------------------------------------------------------------------
/.github/workflows/test-and-release.yaml:
--------------------------------------------------------------------------------
1 | name: Test → Docs → Release
2 |
3 | on:
4 | - pull_request
5 | - push
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
9 | cancel-in-progress: true
10 |
11 | env:
12 | REBAR_VERSION: '3.23.0'
13 | LATEST_ERLANG_VERSION: '27'
14 |
15 | jobs:
16 | # `env_to_output` works around a limitation of GitHub Actions that prevents
17 | # the use of environment variables in places such as a workflow call's `with`
18 | # arguments.
19 | #
20 | # https://github.com/actions/runner/issues/1189#issuecomment-1832389701
21 | env_to_output:
22 | name: Env. variable to outputs
23 | runs-on: ubuntu-latest
24 | outputs:
25 | REBAR_VERSION: ${{ steps.from_env.outputs.REBAR_VERSION }}
26 | steps:
27 | - id: from_env
28 | run: |
29 | vars="
30 | REBAR_VERSION
31 | "
32 | setOutput() {
33 | echo "${1}=${!1}" >> "${GITHUB_OUTPUT}"
34 | }
35 | for name in $vars; do
36 | setOutput $name
37 | done
38 |
39 | test:
40 | name: Test
41 | needs: env_to_output
42 | uses: ./.github/workflows/test-job.yaml
43 | with:
44 | rebar_version: ${{ needs.env_to_output.outputs.REBAR_VERSION }}
45 | secrets:
46 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
47 |
48 | dialyzer:
49 | name: Dialyzer
50 | runs-on: ubuntu-latest
51 | needs: env_to_output
52 |
53 | steps:
54 | - uses: actions/checkout@v4
55 | - uses: erlef/setup-beam@v1
56 | id: install-erlang
57 | with:
58 | otp-version: ${{ env.LATEST_ERLANG_VERSION }}
59 | rebar3-version: ${{ env.REBAR_VERSION }}
60 |
61 | - name: Restore Dialyzer PLT files from cache
62 | uses: actions/cache@v4
63 | with:
64 | path: _build/*/rebar3_*_plt
65 | key: dialyzer-plt-cache-${{ steps.install-erlang.outputs.otp-version }}-${{ runner.os }}-${{ hashFiles('rebar.config*') }}-v1
66 |
67 | - name: Dialyzer
68 | run: rebar3 clean && rebar3 as test dialyzer
69 |
70 | build_docs:
71 | name: Generate docs
72 | runs-on: ubuntu-latest
73 | needs:
74 | - test
75 | - dialyzer
76 |
77 | steps:
78 | - uses: actions/checkout@v4
79 | - uses: erlef/setup-beam@v1
80 | with:
81 | otp-version: ${{ env.LATEST_ERLANG_VERSION }}
82 | rebar3-version: ${{ env.REBAR_VERSION }}
83 |
84 | - name: Change doc version to "Development branch"
85 | run: sed -E -i -e 's/^@version.*/@version Development branch/' doc/overview.edoc
86 |
87 | - name: Generate
88 | run: rebar3 edoc
89 |
90 | - name: Ensure HTML files are there
91 | run: ls -l doc && test -f doc/index.html
92 |
93 | - name: Upload docs for next job
94 | uses: actions/upload-artifact@v4
95 | with:
96 | name: docs_dir
97 | path: ./doc
98 | if-no-files-found: error
99 |
100 | publish_docs:
101 | name: Publish docs
102 | runs-on: ubuntu-latest
103 | needs: build_docs
104 | if: github.repository == 'rabbitmq/horus' && github.ref == 'refs/heads/main'
105 |
106 | steps:
107 | - name: Download docs from previous job
108 | uses: actions/download-artifact@v4
109 | with:
110 | name: docs_dir
111 | path: ./doc
112 |
113 | - name: Ensure HTML files are there
114 | run: ls -l doc && test -f doc/index.html
115 |
116 | - name: Publish
117 | uses: peaceiris/actions-gh-pages@v4
118 | with:
119 | github_token: ${{ secrets.GITHUB_TOKEN }}
120 | publish_dir: ./doc
121 |
122 | publish_release:
123 | name: Publish release
124 | runs-on: ubuntu-latest
125 | needs:
126 | - test
127 | - dialyzer
128 | - build_docs
129 | if: github.repository == 'rabbitmq/horus' && (startsWith(github.ref, 'refs/tags/v0') || startsWith(github.ref, 'refs/tags/v1') || startsWith(github.ref, 'refs/tags/v2') || startsWith(github.ref, 'refs/tags/v3') || startsWith(github.ref, 'refs/tags/v4') || startsWith(github.ref, 'refs/tags/v5') || startsWith(github.ref, 'refs/tags/v6') || startsWith(github.ref, 'refs/tags/v7') || startsWith(github.ref, 'refs/tags/v8') || startsWith(github.ref, 'refs/tags/v9'))
130 |
131 | steps:
132 | - uses: actions/checkout@v4
133 | - uses: erlef/setup-beam@v1
134 | id: install-erlang
135 | with:
136 | otp-version: ${{ env.LATEST_ERLANG_VERSION }}
137 | rebar3-version: ${{ env.REBAR_VERSION }}
138 |
139 | - name: Publish to Hex.pm
140 | env:
141 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
142 | run: rebar3 edoc && rebar3 hex publish -r hexpm --yes
143 |
--------------------------------------------------------------------------------
/.github/workflows/test-job.yaml:
--------------------------------------------------------------------------------
1 | name: Single test job
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | rebar_version:
7 | required: true
8 | type: string
9 | secrets:
10 | CODECOV_TOKEN:
11 | required: true
12 |
13 | jobs:
14 | test:
15 | name: "Erlang/OTP ${{ matrix.otp_version }} + ${{ matrix.os }}"
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | otp_version: ['25', '26', '27']
21 | os: [ubuntu-latest, windows-latest]
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 | - uses: erlef/setup-beam@v1
26 | id: install-erlang
27 | with:
28 | otp-version: ${{ matrix.otp_version }}
29 | rebar3-version: ${{ inputs.rebar_version }}
30 |
31 | - name: Compile
32 | run: rebar3 compile
33 |
34 | - name: Xref
35 | run: rebar3 xref
36 | - name: EUnit (unit tests)
37 | run: env ERL_FLAGS='-enable-feature maybe_expr' rebar3 eunit --verbose --cover
38 | - name: Common test (integration tests)
39 | run: rebar3 ct --verbose --cover --sname ct
40 |
41 | - name: Upload common_test logs
42 | uses: actions/upload-artifact@v4
43 | if: ${{ always() }}
44 | with:
45 | name: common-test-logs-${{ matrix.otp_version }}-${{ matrix.os }}
46 | path: _build/test/logs
47 | include-hidden-files: true
48 | if-no-files-found: ignore
49 | retention-days: 5
50 |
51 | - name: Generate code coverage report
52 | run: rebar3 as test covertool generate
53 |
54 | - name: Upload code coverage to Codecov
55 | uses: codecov/codecov-action@v4
56 | with:
57 | token: ${{ secrets.CODECOV_TOKEN }}
58 | files: _build/test/covertool/horus.covertool.xml
59 | flags: erlang-${{ matrix.otp_version }},os-${{ matrix.os }}
60 | name: Erlang/OTP ${{ matrix.otp_version }} on ${{ matrix.os }}
61 | verbose: true # optional (default = false)
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | erl_crash.dump
2 | .sw?
3 | .*.sw?
4 | /deps
5 | /_build
6 | /_checkouts
7 | /doc/*
8 | !/doc/_head.html
9 | !/doc/horus-favicon.svg
10 | !/doc/horus-logo.svg
11 | !/doc/horus-social-preview.svg
12 | !/doc/horus-social-preview.png
13 | !/doc/overview.edoc
14 | !/doc/stylesheet.css
15 | /mix.lock
16 | /nonode@nohost/
17 |
--------------------------------------------------------------------------------
/LICENSE-Apache-2.0:
--------------------------------------------------------------------------------
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 © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
191 | refers to Broadcom Inc. and/or its subsidiaries.
192 |
193 | Licensed under the Apache License, Version 2.0 (the "License");
194 | you may not use this file except in compliance with the License.
195 | You may obtain a copy of the License at
196 |
197 | http://www.apache.org/licenses/LICENSE-2.0
198 |
199 | Unless required by applicable law or agreed to in writing, software
200 | distributed under the License is distributed on an "AS IS" BASIS,
201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202 | See the License for the specific language governing permissions and
203 | limitations under the License.
204 |
--------------------------------------------------------------------------------
/LICENSE-MPL-2.0:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Horus: anonymous function to standalone module
2 |
3 | [](https://hex.pm/packages/horus/)
4 | [](https://github.com/rabbitmq/horus/actions/workflows/test-and-release.yaml)
5 | [](https://codecov.io/gh/rabbitmq/horus)
6 |
7 | Horus is a library that extracts an anonymous function's code as well as the
8 | code of the all the functions it calls, and creates a standalone version of it
9 | in a new module at runtime.
10 |
11 | The goal is to have a storable and transferable function which does not depend
12 | on the availability of the modules that defined it or were called.
13 |
14 |
15 |
16 | ## How does it work?
17 |
18 | To achieve that goal, Horus extracts the assembly code of the anonymous
19 | function, watches all calls it does and recursively extracts the assembly code
20 | of other called functions. When complete, it creates a standalone Erlang module
21 | based on it. This module can be stored, transfered to another Erlang node and
22 | executed anywhere without the presence of the initial anonymous function's
23 | module.
24 |
25 | If the extracted function calls directly or indirectly modules from the `erts`,
26 | `kernel` or `stdlib` applications, the called functions are not extracted.
27 | That's ok because the behavior of Erlang/OTP modules rarely changes and they
28 | will be available. Therefore, there is little value in extracting that code.
29 |
30 | While processing the assembly instructions and watching function calls, Horus
31 | can use callbacks provided by the caller to determine if instructions and calls
32 | are allowed or denied.
33 |
34 | ## Project maturity
35 |
36 | Horus is still under active development and should be considered *Alpha* at
37 | this stage.
38 |
39 | ## Documentation
40 |
41 | * A short tutorial in the [Getting started](#getting-started) section below
42 | * [Documentation and API reference](https://rabbitmq.github.io/horus/)
43 |
44 | ## Getting started
45 |
46 | ### Add as a dependency
47 |
48 | Add Horus as a dependency of your project:
49 |
50 | Using Rebar:
51 |
52 | ```erlang
53 | %% In rebar.config
54 | {deps, [{horus, "0.3.1"}]}.
55 | ```
56 |
57 | Using Erlang.mk:
58 |
59 | ```make
60 | # In your Makefile
61 | DEPS += horus
62 | dep_horus = hex 0.3.1
63 | ```
64 |
65 | Using Mix:
66 |
67 | ```elixir
68 | # In mix.exs
69 | defp deps do
70 | [
71 | {:horus, "0.3.1"}
72 | ]
73 | end
74 | ```
75 |
76 | ### Extract an anonymous function
77 |
78 | To extract an anonymous function, use `horus:to_standalone_fun/1`:
79 |
80 | ```erlang
81 | Fun = fun() ->
82 | do_something_fancy()
83 | end,
84 |
85 | StandaloneFun = horus:to_standalone_fun(Fun).
86 | ```
87 |
88 | It works with references to regular functions are well:
89 |
90 | ```erlang
91 | Log = fun logger:info/2,
92 |
93 | StandaloneLog = horus:to_standalone_fun(Log).
94 | ```
95 |
96 | ### Execute a standalone function
97 |
98 | Once extracted, the function can be stored as an Erlang binary, or transfered
99 | to a remote Erlang node. You then use `horus:exec/2` to execute it:
100 |
101 | ```erlang
102 | receive
103 | {standalone_fun, StandaloneLog} ->
104 | horus:exec(
105 | StandaloneLog,
106 | ["~p received and executed function", [self()]])
107 | end.
108 | ```
109 |
110 | ## How to build
111 |
112 | ### Build
113 |
114 | ```
115 | rebar3 compile
116 | ```
117 |
118 | ### Build documentation
119 |
120 | ```
121 | rebar3 edoc
122 | ```
123 |
124 | ### Test
125 |
126 | ```
127 | rebar3 xref
128 | rebar3 eunit
129 | rebar3 ct --sname ct
130 | rebar3 as test dialyzer
131 | ```
132 |
133 | ## Copyright and License
134 |
135 | © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" refers to
136 | Broadcom Inc. and/or its subsidiaries.
137 |
138 | This work is dual-licensed under the Apache License 2.0 and the Mozilla Public
139 | License 2.0. You can choose between one of them if you use this work.
140 |
141 | SPDX-License-Identifier: Apache-2.0 OR MPL-2.0
142 |
--------------------------------------------------------------------------------
/doc/_head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/doc/horus-favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
69 |
--------------------------------------------------------------------------------
/doc/horus-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | fun() -module().
91 |
--------------------------------------------------------------------------------
/doc/horus-social-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rabbitmq/horus/823119cac343e8326e29eb9b413d1e6db6533b89/doc/horus-social-preview.png
--------------------------------------------------------------------------------
/doc/horus-social-preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | fun() -module(). rabbitmq/horus
132 |
--------------------------------------------------------------------------------
/doc/overview.edoc:
--------------------------------------------------------------------------------
1 | @author Jean-Sébastien Pédron
2 | @author Michael Davis
3 | @author The RabbitMQ team
4 | @copyright 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
5 | @title The Horus library
6 | @version 0.3.1
7 |
8 | @doc
9 | Horus is a library that extracts an anonymous function's code as well as the
10 | code of the all the functions it calls, and creates a standalone version of it
11 | in a new module at runtime.
12 |
13 | The goal is to have a storable and transferable function which does not depend
14 | on the availability of the modules that defined it or were called.
15 |
16 | Fork me on GitHub
17 |
18 | == How does it work? ==
19 |
20 | To achieve that goal, Horus extracts the assembly code of the anonymous
21 | function, watches all calls it does and recursively extracts the assembly code
22 | of other called functions. When complete, it creates a standalone Erlang module
23 | based on it. This module can be stored, transfered to another Erlang node and
24 | executed anywhere without the presence of the initial anonymous function's
25 | module.
26 |
27 | If the extracted function calls directly or indirectly modules from the `erts',
28 | `kernel' or `stdlib' applications, the called functions are not extracted.
29 | That's ok because the behavior of Erlang/OTP modules rarely changes and they
30 | will be available. Therefore, there is little value in extracting that code.
31 |
32 | While processing the assembly instructions and watching function calls, Horus
33 | can use callbacks provided by the caller to determine if instructions and calls
34 | are allowed or denied.
35 |
36 | === The extraction process ===
37 |
38 | Here is what it does in more details:
39 |
40 | The assembly code of the module hosting the anonymous function is
41 | extracted.
42 | The anonymous function code is located inside that assembly code.
43 | Optionaly, the code is analyzed to determine if it matches constraints
44 | defined by the caller. For instance, it does not perform any forbidden
45 | operations like:
46 |
47 | sending or receiving inter-process messages
48 | accessing files or network connections
49 | calling forbidden functions
50 |
51 | Based on the listed function calls, the same steps are repeated for all of
52 | them (extract, verify, list calls).
53 | Optionaly, once all the assembly code to have a standalone anonymous
54 | function is collected, it checks if it is still needed by the caller.
55 | Finally, an Erlang module is compiled.
56 |
57 |
58 | === Caching ===
59 |
60 | Horus caches assembly code and generated modules. This avoids the need to call
61 | the code server or the compiler again and again.
62 |
63 | Note that at this point, the cache is never invalidated and purged. Memory
64 | management of the cache will be a future improvement.
65 |
66 | === Extraction and code reloading ===
67 |
68 | In the process, Horus pays attention to modules' checksums. Therefore, if a
69 | module is reloaded during a code upgrade, a new standalone function will be
70 | generated from the apparently same anonymous function. This applies to all
71 | modules called by that anonymous function.
72 |
73 | === The anonymous function environment ===
74 |
75 | It's possible for an anonymous function to take inputs from arguments of
76 | course, but also from the scope it is defined in:
77 |
78 | ```
79 | Pid = self(),
80 | Fun = fun() ->
81 | Pid ! stop
82 | end,
83 | '''
84 |
85 | It is even possible to take another anonymous function from the scope:
86 | ```
87 | InnerFun = fun(Ret) ->
88 | {ok, Ret}
89 | end,
90 | OuterFun = fun(Ret) ->
91 | InnerFun(Ret)
92 | end,
93 | '''
94 |
95 | These variables are "stored" in the anonymous function's environment by Erlang.
96 | They are also taken into account by Horus during the extraction. In particular,
97 | if the environment references another anonymous function, it will be extracted
98 | too.
99 |
100 | The returned standalone function contains a standalone copy of the environment.
101 | Thus you don't have to worry about.
102 |
103 | However, that environment is not part of the generated module. Therefore, a
104 | single module will generated for each of `InnerFun' and `OuterFun' from the
105 | example above.
106 |
107 | This avoids the multiplication of generated modules which are loaded at
108 | execution time. With this distinction, a single module is generated and loaded.
109 | The environment stored in the returned standalone function is then passed to
110 | that generated module during execution.
111 |
112 | == Example of a generated module ==
113 |
114 | Let's take the simplest anonymous function:
115 |
116 | ```
117 | fun() ->
118 | ok
119 | end
120 | '''
121 |
122 | Horus generates the following assembly form:
123 |
124 | ```
125 | {
126 | %% This is the generated module name. It is created from the name and
127 | %% origin of the extraction function and a hash. The hash permits
128 | %% several copies of the same function after code reloading.
129 | 'horus__erl_eval__-expr/6-fun-2-__97040876',
130 |
131 | %% This is the list of exported functions. `run/0' is the entry point
132 | %% corresponding to the extracted function and has the same arity (or
133 | %% perhaps a greater arity if the anonymous function took variables
134 | %% from the scope).
135 | %%
136 | %% There could be other non-exported functions in the module corresponding
137 | %% to the functions called by the top-level anonymous function.
138 | [{run,0},
139 | {module_info,0},
140 | {module_info,1}],
141 |
142 | %% These are module attributes. Things like `-dialyzer(...).'. There are
143 | %% none in the generated module.
144 | [],
145 |
146 | %% The actual functions present in the module at last!
147 | [
148 | {function,
149 | run, %% The name of the function.
150 | 0, %% Its arity.
151 | 2, %% The label where the code starts.
152 | [
153 | %% Some metadata for this function:
154 | {label,1},
155 | {func_info,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'},
156 | {atom,run},
157 | 0},
158 | %% The function body for all its clauses.
159 | {label,2},
160 | {move,{atom,ok},{x,0}},
161 | return]},
162 |
163 | %% The `module/{0,1}' functions are added so the generated module is
164 | %% compatible with debuggers.
165 | {function,module_info,0,4,
166 | [{label,3},
167 | {line,[{location,"horus.erl",0}]},
168 | {func_info,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'},
169 | {atom,module_info},
170 | 0},
171 | {label,4},
172 | {move,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'},
173 | {x,0}},
174 | {call_ext_only,1,{extfunc,erlang,get_module_info,1}}]},
175 |
176 | {function,module_info,1,6,
177 | [{label,5},
178 | {line,[{location,"horus.erl",0}]},
179 | {func_info,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'},
180 | {atom,module_info},
181 | 1},
182 | {label,6},
183 | {move,{x,0},{x,1}},
184 | {move,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'},
185 | {x,0}},
186 | {call_ext_only,2,{extfunc,erlang,get_module_info,2}}]}],
187 | 7}}}
188 | '''
189 |
190 | == Why not store/send the module defining the function? ==
191 |
192 | Here is a description of the usecase that started it all.
193 |
194 | Horus doesn't extract the anonymous function code only, but all the functions
195 | it calls, whether they sit in the same module or another module. At the same
196 | time, it will "analyse" the code and give the caller (through callbacks) the
197 | opportunity to deny specific operations or function calls.
198 |
199 | For instance, Khepri — which Horus was created for initially — needs to do that
200 | as part of the transaction functions feature. In case you don't know, Khepri is
201 | a key/value store where keys are organized in a tree. The replication of data
202 | relies on the Raft algorithm. Raft is based on state machines where a leader
203 | state machine sends a journal of commands to followers. The leader and follower
204 | state machines modify their state after applying comands and they must all
205 | reach the exact same state. They also must reach the same state again if the
206 | journal of commands needs to be replayed. You can learn more from the
207 | Khepri documentation .
208 |
209 | For transaction functions to fullfill this "reach same state" constraint no
210 | matter the node running the transaction function, no matter the date/time or
211 | the number of times the function is executed, we need to deny any operations
212 | with side effects or taking input from or having output to something external
213 | to the state machine. For example:
214 |
215 |
216 | the transaction function can't send or receive messages to/from other
217 | processes
218 | it can't perform any disk I/O
219 | it can't use e.g. `persistent_term', `self()', `nodes()', the current time,
220 | etc.
221 |
222 |
223 | We also need to ensure that the transaction function remains the same if it is
224 | executed again in the future, even after an upgrade.
225 |
226 | This is where Horus comes into play. Its role is to collect the entire code of
227 | the transaction function even if it is split into multiple Erlang functions,
228 | accross multiple Erlang modules. This is to prevent that an upgrade changes the
229 | behavior of a transaction function.
230 |
231 | While Horus collects the code, it uses callbacks provided by the caller to let
232 | it say if an operation is allowed or denied. Khepri will deny messages being
233 | sent or received, calls to functions such as `self()' or `node()' and calls to
234 | any functions Khepri doesn't approve.
235 |
236 | By default, Horus will stop following calls (for code extraction) when the code
237 | calls a module provided by the `erts', `kernel' or `stdlib' applications. The
238 | collected code will keep a regular external function call in this case. This is
239 | to avoid the extraction of code we know have no side effects and the behavior
240 | will not change between upgrades. Also because some functions can't be
241 | extracted because they are simple placeholders replaced by NIFs at runtime.
242 |
--------------------------------------------------------------------------------
/doc/stylesheet.css:
--------------------------------------------------------------------------------
1 | body > h2.indextitle:first-of-type
2 | {
3 | font-size: 2em;
4 | text-align: center;
5 | text-transform: capitalize;
6 | border-bottom: 0px;
7 |
8 | padding-top: 115px;
9 | background-image: url("horus-logo.svg");
10 | background-size: auto 100px;
11 | background-repeat: no-repeat;
12 | background-position: top;
13 | }
14 |
15 | .navbar
16 | {
17 | background-color: rgb(245, 242, 240);
18 | border-radius: 6px;
19 | }
20 |
21 | .navbar table tr
22 | {
23 | background-color: transparent;
24 | }
25 |
26 | .navbar table td:last-of-type
27 | {
28 | display: none;
29 | }
30 |
31 | .navbar + hr
32 | {
33 | display: none;
34 | }
35 |
--------------------------------------------------------------------------------
/include/horus.hrl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -ifndef(HORUS_HRL).
10 | -define(HORUS_HRL, true).
11 |
12 | -define(IS_HORUS_FUN(Fun),
13 | (is_function(Fun) orelse ?IS_HORUS_STANDALONE_FUN(Fun))).
14 |
15 | -define(IS_HORUS_STANDALONE_FUN(Fun),
16 | (is_tuple(Fun) andalso
17 | size(Fun) =:= 8 andalso
18 | element(1, Fun) =:= horus_fun)).
19 |
20 | -define(IS_HORUS_STANDALONE_FUN(Fun, Arity),
21 | (?IS_HORUS_STANDALONE_FUN(Fun) andalso
22 | ?HORUS_STANDALONE_FUN_ARITY(Fun) =:= Arity)).
23 |
24 | -define(HORUS_STANDALONE_FUN_ARITY(Fun),
25 | element(4, Fun)).
26 |
27 | -define(horus_error(Name, Props), {horus, Name, Props}).
28 | -define(horus_exception(Name, Props), {horus_ex, Name, Props}).
29 |
30 | -endif. % defined(HORUS_HRL).
31 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Horus.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | # To avoid duplication, we query the app file to learn the application
6 | # name, description and version.
7 | {:ok, [app]} = :file.consult("src/horus.app.src")
8 | {:application, app_name, props} = app
9 |
10 | description = to_string(Keyword.get(props, :description))
11 | version = to_string(Keyword.get(props, :vsn))
12 |
13 | [
14 | app: app_name,
15 | description: description,
16 | version: version,
17 | language: :erlang,
18 | deps: deps()
19 | ]
20 | end
21 |
22 | def application do
23 | {:ok, [app]} = :file.consult("src/horus.app.src")
24 | {:application, _app_name, props} = app
25 |
26 | Keyword.take(props, [:applications, :env, :mod, :registered])
27 | end
28 |
29 | defp deps() do
30 | # To avoid duplication, we query rebar.config to get the list of
31 | # dependencies and their version pinning.
32 | {:ok, terms} = :file.consult("rebar.config")
33 | deps = Keyword.get(terms, :deps)
34 |
35 | # The conversion to the mix.exs expected structure is basic, but that
36 | # should do it for our needs.
37 | for {app_name, version} <- deps do
38 | case version do
39 | _ when is_list(version) ->
40 | {app_name, to_string(version)}
41 |
42 | {:git, url} ->
43 | {app_name, git: to_string(url)}
44 |
45 | {:git, url, {:ref, ref}} ->
46 | {app_name, git: to_string(url), ref: to_string(ref)}
47 |
48 | {:git, url, {:branch, branch}} ->
49 | {app_name, git: to_string(url), branch: to_string(branch)}
50 |
51 | {:git, url, {:tag, tag}} ->
52 | {app_name, git: to_string(url), tag: to_string(tag)}
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/priv/horus_cover_helper.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(horus_cover_helper).
10 |
11 | -export([entry_point/0]).
12 |
13 | entry_point() ->
14 | ok.
15 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %% vim:ft=erlang:
2 | {minimum_otp_vsn, "24.0"}.
3 |
4 | {deps, []}.
5 |
6 | {project_plugins, [covertool,
7 | rebar3_hex,
8 | {rebar3_edoc_extensions, "1.6.1"}]}.
9 |
10 | {erl_opts, [debug_info,
11 | warn_export_vars,
12 | warnings_as_errors]}.
13 |
14 | {dialyzer, [{warnings, [underspecs,
15 | unmatched_returns]}]}.
16 |
17 | {xref_checks, [undefined_function_calls,
18 | undefined_functions,
19 | locals_not_used,
20 | deprecated_function_calls,
21 | deprecated_functions]}.
22 |
23 | {cover_enabled, true}.
24 | {cover_opts, [verbose]}.
25 | {cover_print_enabled, true}.
26 | {cover_export_enabled, true}.
27 | {covertool, [{coverdata_files, ["eunit.coverdata",
28 | "ct.coverdata"]}]}.
29 |
30 | {edoc_opts, [{stylesheet, "stylesheet.css"},
31 | {preprocess, true},
32 | {includes, ["."]},
33 | {sort_functions, false},
34 | {doclet, edoc_doclet_chunks},
35 | {layout, edoc_layout_chunks}]}.
36 |
37 | {alias, [{check, [xref,
38 | {eunit, "-c"},
39 | {cover, "-v --min_coverage=75"},
40 | %% FIXME: Dialyzer is only executed on the library by
41 | %% default, not its testsuite. To run Dialyzer on the
42 | %% testsuites as well, the following command must be used:
43 | %% rebar as test dialyzer
44 | dialyzer,
45 | edoc]}]}.
46 |
47 | {profiles,
48 | [{test,
49 | [{deps, [%% FIXME: We need to add `cth_readable' as a dependency and an
50 | %% extra app for Dialyzer. That's because Rebar is using that
51 | %% application to override `ct:pal()' and Dialyzer complains it
52 | %% doesn't know this application.
53 | cth_readable]},
54 | {dialyzer, [{plt_extra_apps, [common_test,
55 | cth_readable, %% <-- See comment above.
56 | edoc,
57 | eunit,
58 | inets,
59 | mnesia,
60 | ssl,
61 | tools, %% <-- For `cover`.
62 | xmerl]}]}
63 | ]}]}.
64 |
65 | {hex, [{doc, edoc}]}.
66 |
--------------------------------------------------------------------------------
/rebar.lock:
--------------------------------------------------------------------------------
1 | [].
2 |
--------------------------------------------------------------------------------
/src/horus.app.src:
--------------------------------------------------------------------------------
1 | %% vim:ft=erlang:sw=2:et:
2 | {application, horus,
3 | [{description, "Creates standalone modules from anonymous functions"},
4 | %% In addition to below, the version needs to be updated in:
5 | %% * README.md
6 | %% * doc/overview.edoc
7 | %% Pay attention to links in particular.
8 | {vsn, "0.3.1"},
9 | {registered, []},
10 | {applications,
11 | [erts,
12 | kernel,
13 | stdlib,
14 | compiler,
15 | tools %% To support cover-compiled modules.
16 | ]},
17 | {env, [{skip_collection_froms_apps, []}]},
18 | {files, [
19 | "README.md", "LICENSE-Apache-2.0", "LICENSE-MPL-2.0", "mix.exs",
20 | "rebar.config", "rebar.lock", "src", "include", "priv"]},
21 | {modules, []},
22 | {licenses, ["Apache-2.0", "MPL-2.0"]},
23 | {links, [{"GitHub", "https://github.com/rabbitmq/horus"}]},
24 | {build_tools, ["rebar3", "mix"]},
25 | {doc, "doc"}
26 | ]}.
27 |
--------------------------------------------------------------------------------
/src/horus_cover.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | %% @private
10 |
11 | -module(horus_cover).
12 |
13 | -include_lib("stdlib/include/assert.hrl").
14 |
15 | -include("src/horus_error.hrl").
16 | -include("src/horus_fun.hrl").
17 |
18 | -export([isolate_cover_instructions/2,
19 | determine_cover_patterns_locally/0]).
20 |
21 | %% -------------------------------------------------------------------
22 | %% Helpers to detect cover-specific instructions.
23 | %% -------------------------------------------------------------------
24 |
25 | -define(COVER_HELPER_MOD, horus_cover_helper).
26 |
27 | -spec isolate_cover_instructions(MFA, Instructions) -> NewInstructions when
28 | MFA :: {Module, Name, Arity},
29 | Module :: module(),
30 | Name :: atom(),
31 | Arity :: arity(),
32 | Instructions :: [horus:beam_instr()],
33 | NewInstructions :: [horus:beam_instr() |
34 | {'$cover', [horus:beam_instr()]}].
35 | %% @doc Finds and isolates cover-specific instructions.
36 | %%
37 | %% If this function finds cover-specific instructions, it will isolate them in
38 | %% a ``{'$cover', [...]}'' tuple. The caller is responsible for processing
39 | %% those isolated instructions however it wants then put them back in the
40 | %% regular flow of instructions.
41 | %%
42 | %% The module named `?COVER_HELPER_MOD' is explicitly ignored by this
43 | %% function to avoid an infinite loop in the method used to determine
44 | %% cover-specific instructions.
45 | %%
46 | %% @private
47 |
48 | isolate_cover_instructions({?COVER_HELPER_MOD, _, _}, Instructions) ->
49 | %% The `?COVER_HELPER_MOD' module is used to determine the cover-specific
50 | %% instructions. As part of this, the code goes through this function too.
51 | %% To avoid an infinite recursion, we special-case this module and don't
52 | %% call {@link get_cover_instructions_patterns/0}.
53 | Instructions;
54 | isolate_cover_instructions(_MFA, Instructions) ->
55 | %% We first get the cached cover instructions patterns (or determine these
56 | %% patterns if this is the first time), then we inspect the given
57 | %% instructions to see if we find cover-specific instructions.
58 | CoverPatterns = get_cover_instructions_patterns(),
59 | isolate_cover_instructions(Instructions, CoverPatterns, []).
60 |
61 | isolate_cover_instructions(Instructions, CoverPatterns, Result)
62 | when length(Instructions) < length(CoverPatterns) ->
63 | %% We are too close to the end of the function body, there are not enough
64 | %% instructions left to ever match the cover-specific instructions
65 | %% patterns.
66 | %%
67 | %% We can return what we have processed so far and append the remaining
68 | %% instructions.
69 | lists:reverse(Result) ++ Instructions;
70 | isolate_cover_instructions(Instructions, CoverPatterns, Result) ->
71 | %% We evaluate the patterns against the head of `Instructions'.
72 | case isolate_cover_instructions1(Instructions, CoverPatterns, []) of
73 | {IsolatedCoverInstructions, Rest} ->
74 | %% We found cover-specific instructions we can isolate. We then
75 | %% continue with the rest of the function body.
76 | Result1 = [{'$cover', IsolatedCoverInstructions} | Result],
77 | isolate_cover_instructions(Rest, CoverPatterns, Result1);
78 | false ->
79 | %% The head of `Instructions' didn't match the patterns. Let's
80 | %% skip one instruction and try again.
81 | [Instruction | Rest] = Instructions,
82 | Result1 = [Instruction | Result],
83 | isolate_cover_instructions(Rest, CoverPatterns, Result1)
84 | end.
85 |
86 | isolate_cover_instructions1(
87 | [Instr | TailInstrs],
88 | [Pattern | TailPatterns] = Patterns,
89 | Result) ->
90 | %% The first instruction must match the first pattern before we consider
91 | %% following patterns.
92 | %%
93 | %% When a pattern matches, we move on to the next pattern until all of
94 | %% them matched an instruction. If we reach the last instructions but not
95 | %% all patterns matched, it means the instructions starting at `Instr' are
96 | %% not a series of cover-specific instructions.
97 | %%
98 | %% There may be other unimportant instructions in between cover-specific
99 | %% instructions, such as `{line, _}' annotations. Except for the first
100 | %% instruction, that's why if an instruction doesn't match a pattern, we
101 | %% put it in the result and move on to the next instruction until a
102 | %% pattern matches.
103 | case ets:match_spec_run([Instr], Pattern) of
104 | [match] ->
105 | %% A pattern matched, we continue with the next pattern.
106 | Result1 = [Instr | Result],
107 | isolate_cover_instructions1(TailInstrs, TailPatterns, Result1);
108 | _ when Result =:= [] ->
109 | %% The first pattern must match the very first instruction. Here,
110 | %% it didn't, thus `Instructions' are not a series of
111 | %% cover-specific instructions.
112 | false;
113 | _ ->
114 | %% The instruction didn't match, but that doesn't rule out a
115 | %% cover-specific series of instructions because there could be
116 | %% unimportant instructions in between.
117 | Result1 = [Instr | Result],
118 | isolate_cover_instructions1(TailInstrs, Patterns, Result1)
119 | end;
120 | isolate_cover_instructions1(Instructions, [], Result) ->
121 | %% All patterns matched, we have a winner!
122 | {lists:reverse(Result), Instructions};
123 | isolate_cover_instructions1([], Patterns, _Result) when Patterns =/= [] ->
124 | %% We reach the end of `Instructions' and some pattern were not matched.
125 | %% This is not a cover-specific series of instructions.
126 | false.
127 |
128 | -define(COVER_INSTRUCTIONS_CACHE_KEY, {horus, asm_cache, cover_instructions}).
129 |
130 | -spec get_cover_instructions_patterns() -> CoverPatterns when
131 | CoverPatterns :: [ets:compiled_match_spec()].
132 | %% @doc Returns the cover-specific instructions patterns.
133 | %%
134 | %% The patterns are cached. If the cache is empty, we acquire a lock and run
135 | %% the function to determine the patterns. The result is cached and the lock
136 | %% is released.
137 | %%
138 | %% @private
139 |
140 | get_cover_instructions_patterns() ->
141 | case persistent_term:get(?COVER_INSTRUCTIONS_CACHE_KEY, undefined) of
142 | CoverPatterns when CoverPatterns =/= undefined ->
143 | CoverPatterns;
144 | undefined ->
145 | Lock = {?COVER_INSTRUCTIONS_CACHE_KEY, self()},
146 | global:set_lock(Lock, [node()]),
147 | try
148 | get_cover_instructions_patterns_locked()
149 | after
150 | global:del_lock(Lock)
151 | end
152 | end.
153 |
154 | -spec get_cover_instructions_patterns_locked() -> CoverPatterns when
155 | CoverPatterns :: [ets:compiled_match_spec()].
156 | %% @doc Returns the cover-specific instructions patterns.
157 | %%
158 | %% It looks at the cache first, then call {@link determine_cover_patterns/0}
159 | %% if the cache is empty.
160 | %%
161 | %% This function must be called with the lock held. Otherwise concurrent calls
162 | %% will fail to determine the cover-specific instructions as they will
163 | %% concurrently cover-compile and load the same module.
164 | %%
165 | %% @private
166 |
167 | get_cover_instructions_patterns_locked() ->
168 | case persistent_term:get(?COVER_INSTRUCTIONS_CACHE_KEY, undefined) of
169 | undefined ->
170 | CoverPatterns = determine_cover_patterns(),
171 | persistent_term:put(?COVER_INSTRUCTIONS_CACHE_KEY, CoverPatterns),
172 | CoverPatterns;
173 | CoverPatterns ->
174 | CoverPatterns
175 | end.
176 |
177 | -spec determine_cover_patterns() -> CoverPatterns when
178 | CoverPatterns :: [ets:compiled_match_spec()].
179 | %% @doc Determines the cover-specific instructions patterns, regardless of the
180 | %% executing node.
181 | %%
182 | %% @private.
183 |
184 | determine_cover_patterns() ->
185 | %% Cover-compilation only works on the main node, thus we may need to
186 | %% perform an RPC call.
187 | CoverPatterns = case cover:get_main_node() of
188 | Node when Node =:= node() ->
189 | determine_cover_patterns_locally();
190 | Node ->
191 | determine_cover_patterns_remotely(Node)
192 | end,
193 | %% However, the compilation of an ETS match spec is only valid locally as
194 | %% it returns a reference.
195 | CoverMatchSpecs = [begin
196 | MatchSpec = {Pattern, [], [match]},
197 | ets:match_spec_compile([MatchSpec])
198 | end || Pattern <- CoverPatterns],
199 | CoverMatchSpecs.
200 |
201 | -spec determine_cover_patterns_locally() -> CoverPatterns when
202 | CoverPatterns :: [any(), ...].
203 | %% @doc Computes the cover-specific instructions patterns.
204 | %%
205 | %% To achieve this, the function uses a very basic module (see
206 | %% `priv/horus_cover_helper.erl').
207 | %%
208 | %% First, it compiles the module with `compile' and extracts its single
209 | %% function using Horus. Then it compiles the same module with `cover' and
210 | %% extracts the function again.
211 | %%
212 | %% It then compares the bodies of the two copies of the same function to
213 | %% determine the instructions added by `cover'.
214 | %%
215 | %% This list of instructions is converted to ETS match patterns. The patterns
216 | %% will be used in {@link isolate_cover_instructions/2}.
217 | %%
218 | %% @private
219 |
220 | determine_cover_patterns_locally() ->
221 | %% Compile the `horus_cover_helper' module at runtime. It is located in
222 | %% the `priv' directory.
223 | %%
224 | %% We can't use an on-disk module in `src' because Rebar will
225 | %% cover-compile it out-of-the-box when executing the testsuite and thus
226 | %% we can't compare it with the copy we cover-compile ourselves.
227 | PrivDir = code:priv_dir(horus),
228 | SourceFile = filename:join(PrivDir, "horus_cover_helper.erl"),
229 | CompileOpts = [binary, return_errors],
230 | {ok, Module, Beam} = compile:file(SourceFile, CompileOpts),
231 | ?assertEqual(?COVER_HELPER_MOD, Module),
232 |
233 | %% We load the module to create the anonymous function reference and
234 | %% extract the standalone function.
235 | {module, Module} = code:load_binary(Module, SourceFile, Beam),
236 | ok = horus:override_object_code(Module, Beam),
237 | Fun = fun Module:entry_point/0,
238 | ShouldProcessFunction = fun
239 | (M, _, _, _) when M =:= Module -> true;
240 | (_, _, _, _) -> false
241 | end,
242 | HorusOpts = #{debug_info => true,
243 | add_module_info => false,
244 | should_process_function => ShouldProcessFunction},
245 | StandaloneFun1 = horus:to_standalone_fun(Fun, HorusOpts),
246 | ok = horus:forget_overridden_object_code(Module),
247 |
248 | %% We do the same with `cover'.
249 | {ok, Module} = cover:compile_module(SourceFile),
250 | StandaloneFun2 = horus:to_standalone_fun(Fun, HorusOpts),
251 | RegularBody = get_entrypoint_body(StandaloneFun1),
252 | CoveredBody = get_entrypoint_body(StandaloneFun2),
253 |
254 | %% We now compare the two function bodies to determine cover-specific
255 | %% instructions.
256 | do_determine_cover_patterns(CoveredBody, RegularBody).
257 |
258 | get_entrypoint_body(StandaloneFun) ->
259 | %% We skip the function header until we reach the label of the body.
260 | Asm = get_entrypoint_asm(StandaloneFun),
261 | Body = lists:dropwhile(
262 | fun
263 | ({label, 2}) -> false;
264 | (_) -> true
265 | end, Asm),
266 | Body.
267 |
268 | get_entrypoint_asm(#horus_fun{debug_info = #{asm := Asm}}) ->
269 | {_, _, _, Functions1, _} = Asm,
270 | Entrypoint = hd(Functions1),
271 | EntrypointAsm = element(5, Entrypoint),
272 | EntrypointAsm.
273 |
274 | do_determine_cover_patterns(CoveredBody, RegularBody) ->
275 | {CoveredBody1, RegularBody1} = eliminate_common_start(
276 | CoveredBody, RegularBody),
277 | CoveredStart = get_cover_specific_start(CoveredBody1, RegularBody1, []),
278 | %% We ensure the cover-specific instructions were found. If the result is
279 | %% an empty list, it means we compared two identical standalone functions,
280 | %% meaning something went wrong with the compilation.
281 | case CoveredStart of
282 | [] ->
283 | ?horus_misuse(
284 | failed_to_determine_cover_specific_instructions,
285 | #{});
286 | _ ->
287 | CoveredStart
288 | end.
289 |
290 | eliminate_common_start(
291 | [Instruction | CoveredRest], [Instruction | RegularRest]) ->
292 | eliminate_common_start(CoveredRest, RegularRest);
293 | eliminate_common_start(CoveredBody, RegularBody) ->
294 | {CoveredBody, RegularBody}.
295 |
296 | get_cover_specific_start(
297 | [Instruction | _CoveredRest], [Instruction | _RegularRest],
298 | CoveredStart) ->
299 | CoveredStart1 = lists:reverse(CoveredStart),
300 | CoverPatterns = instructions_to_patterns(CoveredStart1),
301 | CoverPatterns;
302 | get_cover_specific_start(
303 | [Instruction | CoveredRest], RegularRest, CoveredStart) ->
304 | CoveredStart1 = [Instruction | CoveredStart],
305 | get_cover_specific_start(CoveredRest, RegularRest, CoveredStart1).
306 |
307 | instructions_to_patterns(CoveredStart) ->
308 | lists:filtermap(
309 | fun
310 | ({move, {literal, {cover, _}}, _}) ->
311 | {true, {move, {literal, {cover, '_'}}, '_'}};
312 | ({call_ext, _, {extfunc, _, _, _}}) ->
313 | {true, {call_ext, '_', {extfunc, '_', '_', '_'}}};
314 | ({move, _, _}) ->
315 | {true, {move, '_', '_'}};
316 | ({allocate, _, _}) ->
317 | false;
318 | ({line, _}) ->
319 | false
320 | end, CoveredStart).
321 |
322 | -spec determine_cover_patterns_remotely(Node) -> CoverPatterns when
323 | Node :: node(),
324 | CoverPatterns :: [any(), ...].
325 | %% @doc Calls `Node' to determine the cover-specific instructions.
326 |
327 | determine_cover_patterns_remotely(Node) ->
328 | erpc:call(Node, ?MODULE, determine_cover_patterns_locally, []).
329 |
--------------------------------------------------------------------------------
/src/horus_cover.hrl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2024-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -if(?OTP_RELEASE >= 27).
10 | -define(IF_NATIVE_COVERAGE_IS_SUPPORTED(IfSupportedBlock, ElseBlock),
11 | (case code:coverage_support() of
12 | true ->
13 | IfSupportedBlock;
14 | false ->
15 | ElseBlock
16 | end)).
17 | -else.
18 | -define(IF_NATIVE_COVERAGE_IS_SUPPORTED(_IfSupportedBlock, ElseBlock),
19 | (ElseBlock)).
20 | -endif.
21 |
--------------------------------------------------------------------------------
/src/horus_error.hrl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2022-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -ifndef(HORUS_ERROR_HRL).
10 | -define(HORUS_ERROR_HRL, true).
11 |
12 | -include("include/horus.hrl").
13 |
14 | -define(
15 | horus_misuse(Exception),
16 | erlang:error(Exception)).
17 |
18 | -define(
19 | horus_misuse(Name, Props),
20 | ?horus_misuse(?horus_exception(Name, Props))).
21 |
22 | -define(
23 | horus_raise_misuse(Name, Props, Stacktrace),
24 | erlang:raise(error, ?horus_exception(Name, Props), Stacktrace)).
25 |
26 | -endif. % defined(HORUS_ERROR_HRL).
27 |
--------------------------------------------------------------------------------
/src/horus_fun.hrl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -ifndef(HORUS_FUN_HRL).
10 | -define(HORUS_FUN_HRL, true).
11 |
12 | %% Structure representing an anonymous function "extracted" as a compiled
13 | %% module for storage.
14 | %%
15 | %% IMPORTANT: When adding or removing fields to this record, be sure to update
16 | %% `include/horus.hrl'!
17 | -record(horus_fun, {module :: module(),
18 | beam :: binary(),
19 | arity :: arity(),
20 | literal_funs :: [horus:horus_fun()],
21 | fun_name_mapping :: horus:fun_name_mapping(),
22 | env :: list(),
23 | debug_info :: horus:debug_info() | undefined}).
24 |
25 | -endif. % defined(HORUS_FUN_HRL).
26 |
--------------------------------------------------------------------------------
/src/horus_utils.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | %% @private
10 |
11 | -module(horus_utils).
12 |
13 | -export([is_module_loaded/1,
14 | should_process_module/1,
15 | init_list_of_modules_to_skip/0,
16 | clear_list_of_modules_to_skip/0]).
17 |
18 | -spec is_module_loaded(Module) -> IsLoaded when
19 | Module :: module(),
20 | IsLoaded :: boolean().
21 | %% @doc Indicates if a module is loaded or not.
22 | %%
23 | %% For Erlang/OTP up to 25, it is an alternative to `code:is_loaded/1' because
24 | %% it is a synchronous call to the code server in these versions.
25 | %%
26 | %% For Erlang/OTP 26+, this is a simply wrapper.
27 | %%
28 | %% @end
29 |
30 | %% We use a compile-time check instead of a runtime check because if the
31 | %% application is compiled with Erlang/OTP 25, `is_module_loaded/1' works
32 | %% equally well on Erlang/OTP 26.
33 | %%
34 | %% If the application is compiled with Erlang/OTP26, then it won't load on
35 | %% Erlang/OTP 25 anyway.
36 |
37 | -if(?OTP_RELEASE >= 26).
38 | is_module_loaded(Module) ->
39 | %% Starting from Erlang/OTP 26, this is a query of a protected ETS table.
40 | case code:is_loaded(Module) of
41 | {file, _} -> true;
42 | false -> false
43 | end.
44 | -else.
45 | is_module_loaded(Module) ->
46 | %% Up to Erlang/OTP 25, this is a synchronous call to the Code server. This
47 | %% is a contention point, so let's use the undocumented
48 | %% `erlang:get_module_info/2' BIF (which is behind any
49 | %% `Module:module_info/1') to test the presence of the module.
50 | try
51 | _ = erlang:get_module_info(Module, md5),
52 | true
53 | catch
54 | error:badarg ->
55 | false
56 | end.
57 | -endif.
58 |
59 | %% -------------------------------------------------------------------
60 | %% Helpers to standalone function extraction.
61 | %% -------------------------------------------------------------------
62 |
63 | -define(PT_MODULES_TO_SKIP, {horus, skipped_modules_in_code_collection}).
64 |
65 | -spec should_process_module(Module) -> Collect when
66 | Module :: module(),
67 | Collect :: boolean().
68 | %% @doc Indicates if the code from `Module' should be processed/collected.
69 |
70 | should_process_module(Module) ->
71 | try
72 | Modules = persistent_term:get(?PT_MODULES_TO_SKIP),
73 | not maps:is_key(Module, Modules)
74 | catch
75 | error:badarg ->
76 | ok = init_list_of_modules_to_skip(),
77 | should_process_module(Module)
78 | end.
79 |
80 | -spec init_list_of_modules_to_skip() -> ok.
81 | %% @doc Initializes a list of modules from Erlang standalone library that
82 | %% should not be collected.
83 |
84 | init_list_of_modules_to_skip() ->
85 | SkippedApps0 = [erts, kernel, stdlib, horus],
86 | SkippedApps1 = application:get_env(horus, skip_collection_froms_apps, []),
87 | SkippedApps = lists:usort(SkippedApps0 ++ SkippedApps1),
88 | Modules = lists:foldl(
89 | fun(App, Modules0) ->
90 | _ = application:load(App),
91 | Mods = case application:get_key(App, modules) of
92 | {ok, List} -> List;
93 | undefined -> []
94 | end,
95 | lists:foldl(
96 | fun(Mod, Modules1) -> Modules1#{Mod => true} end,
97 | Modules0, Mods)
98 | end, #{}, SkippedApps),
99 | persistent_term:put(?PT_MODULES_TO_SKIP, Modules),
100 | ok.
101 |
102 | -spec clear_list_of_modules_to_skip() -> ok.
103 | %% @doc Clears the cached list of modules to not collect.
104 |
105 | clear_list_of_modules_to_skip() ->
106 | _ = persistent_term:erase(?PT_MODULES_TO_SKIP),
107 | ok.
108 |
--------------------------------------------------------------------------------
/test/arbitrary_mod.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(arbitrary_mod).
10 |
11 | -export([min/2,
12 | call_inner_function/2]).
13 |
14 | min(A, B) ->
15 | erlang:min(A, B).
16 |
17 | call_inner_function(InnerFun, Options) when is_map(Options) ->
18 | InnerFun(Options).
19 |
--------------------------------------------------------------------------------
/test/cached_extractions.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(cached_extractions).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | -dialyzer({nowarn_function, [my_fun_module/1,
16 | modified_module_causes_cache_miss_test/0]}).
17 |
18 | local_fun_test() ->
19 | Fun = fun() -> ok end,
20 | StandaloneFun1 = horus:to_standalone_fun(Fun),
21 | StandaloneFun2 = horus:to_standalone_fun(Fun),
22 | ?assertEqual(StandaloneFun1, StandaloneFun2),
23 | ?assertEqual(ok, horus:exec(StandaloneFun1, [])),
24 | ?assertEqual(ok, horus:exec(StandaloneFun2, [])).
25 |
26 | external_fun_test() ->
27 | Fun = fun erlang:abs/1,
28 | StandaloneFun1 = horus:to_standalone_fun(Fun),
29 | StandaloneFun2 = horus:to_standalone_fun(Fun),
30 | ?assertEqual(StandaloneFun1, StandaloneFun2),
31 | ?assertEqual(1, horus:exec(StandaloneFun1, [1])),
32 | ?assertEqual(1, horus:exec(StandaloneFun2, [1])).
33 |
34 | standalone_fun_is_cached_test() ->
35 | Fun = fun() -> ok end,
36 |
37 | #{module := Module,
38 | name := Name,
39 | arity := Arity,
40 | type := local,
41 | new_uniq := Checksum} = maps:from_list(erlang:fun_info(Fun)),
42 | Options = #{},
43 | Key = horus:standalone_fun_cache_key(
44 | Module, Name, Arity, Checksum, Options),
45 |
46 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options),
47 | CacheEntry1 = persistent_term:get(Key, undefined),
48 | ?assertStandaloneFun(StandaloneFun1),
49 | ?assertMatch(#{horus_fun := StandaloneFun1}, CacheEntry1),
50 | #{counters := Counters} = CacheEntry1,
51 | ?assertEqual(0, counters:get(Counters, 1)),
52 |
53 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options),
54 | CacheEntry2 = persistent_term:get(Key, undefined),
55 | ?assertEqual(StandaloneFun1, StandaloneFun2),
56 | ?assertEqual(CacheEntry1, CacheEntry2),
57 | ?assertEqual(1, counters:get(Counters, 1)),
58 |
59 | StandaloneFun3 = horus:to_standalone_fun(Fun, Options),
60 | CacheEntry3 = persistent_term:get(Key, undefined),
61 | ?assertEqual(StandaloneFun1, StandaloneFun3),
62 | ?assertEqual(CacheEntry1, CacheEntry3),
63 | ?assertEqual(2, counters:get(Counters, 1)).
64 |
65 | kept_fun_is_cached_test() ->
66 | Fun = fun() -> ok end,
67 |
68 | #{module := Module,
69 | name := Name,
70 | arity := Arity,
71 | type := local,
72 | new_uniq := Checksum} = maps:from_list(erlang:fun_info(Fun)),
73 | Options = #{should_process_function =>
74 | fun(_Module, _Function, _Arity, _FromModule) -> false end},
75 | Key = horus:standalone_fun_cache_key(
76 | Module, Name, Arity, Checksum, Options),
77 |
78 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options),
79 | CacheEntry1 = persistent_term:get(Key, undefined),
80 | ?assertEqual(Fun, StandaloneFun1),
81 | ?assertMatch(#{fun_kept := true}, CacheEntry1),
82 | #{counters := Counters} = CacheEntry1,
83 | ?assertEqual(0, counters:get(Counters, 1)),
84 |
85 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options),
86 | CacheEntry2 = persistent_term:get(Key, undefined),
87 | ?assertEqual(StandaloneFun1, StandaloneFun2),
88 | ?assertEqual(CacheEntry1, CacheEntry2),
89 | ?assertEqual(1, counters:get(Counters, 1)),
90 |
91 | StandaloneFun3 = horus:to_standalone_fun(Fun, Options),
92 | CacheEntry3 = persistent_term:get(Key, undefined),
93 | ?assertEqual(StandaloneFun1, StandaloneFun3),
94 | ?assertEqual(CacheEntry1, CacheEntry3),
95 | ?assertEqual(2, counters:get(Counters, 1)).
96 |
97 | different_options_means_different_cache_entries_test() ->
98 | Fun = fun() -> ok end,
99 |
100 | Options1 = #{},
101 | Options2 = #{should_process_function =>
102 | fun(_Module, _Function, _Arity, _FromModule) -> false end},
103 |
104 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options1),
105 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options2),
106 | ?assertStandaloneFun(StandaloneFun1),
107 | ?assertEqual(Fun, StandaloneFun2).
108 |
109 | my_fun_module(Version) ->
110 | Module = my_fun,
111 | Asm = {Module, %% Module
112 | [{module_info,0}, %% Exports
113 | {module_info,1},
114 | {version,0}],
115 | [], %% Attributes
116 | [
117 | {function, version, 0, 2,
118 | [
119 | {label, 1},
120 | {func_info, {atom, Module}, {atom, version},0},
121 | {label, 2},
122 | {move, {integer, Version}, {x, 0}},
123 | return
124 | ]},
125 | {function, module_info, 0, 4,
126 | [
127 | {label, 3},
128 | {func_info, {atom, Module}, {atom, module_info}, 0},
129 | {label, 4},
130 | {move, {atom, Module}, {x, 0}},
131 | {call_ext_only, 1, {extfunc, erlang, get_module_info, 1}}
132 | ]},
133 | {function, module_info, 1, 6,
134 | [
135 | {label, 5},
136 | {func_info, {atom, Module}, {atom, module_info}, 1},
137 | {label, 6},
138 | {move, {x, 0}, {x, 1}},
139 | {move, {atom, Module}, {x, 0}},
140 | {call_ext_only, 2, {extfunc, erlang, get_module_info, 2}}
141 | ]}
142 | ], %% Functions
143 | 7 %% Label
144 | },
145 | horus:compile(Asm).
146 |
147 | modified_module_causes_cache_miss_test() ->
148 | {Module, Beam1} = my_fun_module(1),
149 | {Module, Beam2} = my_fun_module(2),
150 |
151 | Options = #{},
152 |
153 | horus:override_object_code(Module, Beam1),
154 | ?assertEqual(
155 | {Module, Beam1, "", code_server},
156 | horus:get_object_code(Module)),
157 | ?assertEqual({module, Module}, code:load_binary(Module, "", Beam1)),
158 | ?assert(erlang:function_exported(Module, version, 0)),
159 | Fun1 = fun Module:version/0,
160 | #{module := Module,
161 | name := Name1,
162 | arity := Arity1,
163 | type := external} = maps:from_list(erlang:fun_info(Fun1)),
164 | Checksum1 = Module:module_info(md5),
165 | Key1 = horus:standalone_fun_cache_key(
166 | Module, Name1, Arity1, Checksum1, Options),
167 |
168 | StandaloneFun1 = horus:to_standalone_fun(Fun1, Options),
169 | CacheEntry1 = persistent_term:get(Key1, undefined),
170 | ?assertStandaloneFun(StandaloneFun1),
171 | ?assertEqual(1, horus:exec(StandaloneFun1, [])),
172 | #{counters := Counters1} = CacheEntry1,
173 | ?assertEqual(0, counters:get(Counters1, 1)),
174 |
175 | true = code:delete(Module),
176 | _ = code:purge(Module),
177 |
178 | horus:override_object_code(Module, Beam2),
179 | ?assertEqual(
180 | {Module, Beam2, "", code_server},
181 | horus:get_object_code(Module)),
182 | ?assertEqual({module, Module}, code:load_binary(Module, "", Beam2)),
183 | ?assert(erlang:function_exported(Module, version, 0)),
184 | Fun2 = fun Module:version/0,
185 | #{module := Module,
186 | name := Name2,
187 | arity := Arity2,
188 | type := external} = maps:from_list(erlang:fun_info(Fun2)),
189 | Checksum2 = Module:module_info(md5),
190 | Key2 = horus:standalone_fun_cache_key(
191 | Module, Name2, Arity2, Checksum2, Options),
192 | ?assertEqual(Name1, Name2),
193 | ?assertEqual(Arity1, Arity2),
194 | ?assertNotEqual(Checksum1, Checksum2),
195 |
196 | StandaloneFun2 = horus:to_standalone_fun(Fun2, Options),
197 | CacheEntry2 = persistent_term:get(Key2, undefined),
198 | ?assertStandaloneFun(StandaloneFun2),
199 | ?assertEqual(2, horus:exec(StandaloneFun2, [])),
200 | #{counters := Counters2} = CacheEntry2,
201 | ?assertEqual(0, counters:get(Counters2, 1)),
202 |
203 | true = code:delete(Module),
204 | _ = code:purge(Module).
205 |
206 | callback_options_impact_cache_key_test() ->
207 | Fun = fun() -> ok end,
208 | Options1 = #{should_process_function =>
209 | fun should_process_function_1/4},
210 | Options2 = #{should_process_function =>
211 | fun should_process_function_2/4},
212 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options1),
213 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options2),
214 | ?assertStandaloneFun(StandaloneFun1),
215 | ?assertStandaloneFun(StandaloneFun2),
216 | ?assertEqual(StandaloneFun1, StandaloneFun2),
217 |
218 | #{module := Module,
219 | name := Name,
220 | arity := Arity,
221 | type := local} = maps:from_list(erlang:fun_info(Fun)),
222 | Checksum = Module:module_info(md5),
223 | Key1 = horus:standalone_fun_cache_key(
224 | Module, Name, Arity, Checksum, Options1),
225 | Key2 = horus:standalone_fun_cache_key(
226 | Module, Name, Arity, Checksum, Options2),
227 | ?assertNotEqual(Key1, Key2),
228 |
229 | CacheEntry1 = persistent_term:get(Key1, undefined),
230 | ?assertMatch(#{horus_fun := StandaloneFun1}, CacheEntry1),
231 | #{counters := Counters1} = CacheEntry1,
232 | ?assertEqual(0, counters:get(Counters1, 1)),
233 |
234 | CacheEntry2 = persistent_term:get(Key2, undefined),
235 | ?assertMatch(#{horus_fun := StandaloneFun2}, CacheEntry2),
236 | #{counters := Counters2} = CacheEntry2,
237 | ?assertEqual(0, counters:get(Counters2, 1)).
238 |
239 | should_process_function_1(_Module, _Function, _Arity, _FromModule) ->
240 | true.
241 |
242 | should_process_function_2(_Module, _Function, _Arity, _FromModule) ->
243 | true.
244 |
--------------------------------------------------------------------------------
/test/cover_compile.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(cover_compile).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("src/horus_cover.hrl").
14 | -include("src/horus_fun.hrl").
15 |
16 | cover_compilation_works_test() ->
17 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED(
18 | begin
19 | _ = code:set_coverage_mode(line_counters),
20 | ok
21 | end,
22 | ok),
23 |
24 | Module = cover_compiled_mod1,
25 | Arg = arg,
26 | Ret = Module:run(Arg),
27 |
28 | Fun = fun Module:run/1,
29 | {_, _, ModFile} = code:get_object_code(Module),
30 | ?assertEqual(false, cover:is_compiled(Module)),
31 | InitialState = [{{Module, run, 1},
32 | {0, %% Covered
33 | 2 %% Not covered
34 | }}],
35 | Analysis = [{{Module, run, 1},
36 | {2, %% Covered
37 | 0 %% Not covered
38 | }}],
39 |
40 | %% We extract the regular module before we cover-compile it. We do this to
41 | %% compare the standalone functions to make sure that their module names
42 | %% are different because the code and the checksum are different.
43 | StandaloneFun1 = horus:to_standalone_fun(Fun, #{debug_info => true}),
44 | ?assertEqual(Ret, horus:exec(StandaloneFun1, [Arg])),
45 |
46 | %% We ensure that cover-compilation works and that we get the expected
47 | %% analysis results.
48 | ?assertEqual({ok, Module}, cover:compile_beam(ModFile)),
49 | ?assertEqual({file, ModFile}, cover:is_compiled(Module)),
50 | ?assertEqual({ok, InitialState}, cover:analyse(Module)),
51 |
52 | ?assertEqual(Ret, Module:run(Arg)),
53 | ?assertEqual({ok, Analysis}, cover:analyse(Module)),
54 |
55 | ok = cover:reset(Module),
56 |
57 | %% Now, we try to extract the cover-compiled module. We then execute the
58 | %% standalone function and verify the analysis again.
59 | ?assertEqual({file, ModFile}, cover:is_compiled(Module)),
60 | ?assertEqual({ok, InitialState}, cover:analyse(Module)),
61 |
62 | StandaloneFun2 = horus:to_standalone_fun(Fun, #{debug_info => true}),
63 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED(
64 | begin
65 | ?debugMsg(
66 | "Coverage support testing skipped as native coverage counters "
67 | "can't be modified externally")
68 | end,
69 | begin
70 | ?assertEqual(Ret, horus:exec(StandaloneFun2, [Arg])),
71 | ?assertEqual({ok, Analysis}, cover:analyse(Module))
72 | end),
73 |
74 | ok = cover:reset(Module),
75 |
76 | %% We finally compare the standalone functions: they must have a different
77 | %% module name and checksum.
78 | GeneratedName1 = StandaloneFun1#horus_fun.module,
79 | GeneratedName2 = StandaloneFun2#horus_fun.module,
80 | ?assertNotEqual(GeneratedName1, GeneratedName2),
81 |
82 | Info1 = maps:get(fun_info, StandaloneFun1#horus_fun.debug_info),
83 | Info2 = maps:get(fun_info, StandaloneFun2#horus_fun.debug_info),
84 | ?assertEqual(Info1, Info2),
85 |
86 | Checksums1 = maps:get(checksums, StandaloneFun1#horus_fun.debug_info),
87 | Checksums2 = maps:get(checksums, StandaloneFun2#horus_fun.debug_info),
88 | ?assertNotEqual(
89 | maps:get(Module, Checksums1),
90 | maps:get(Module, Checksums2)),
91 |
92 | ok.
93 |
94 | cover_on_remote_node_works_test() ->
95 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED(
96 | begin
97 | _ = code:set_coverage_mode(line_counters),
98 | ok
99 | end,
100 | ok),
101 |
102 | ok = helpers:start_epmd(),
103 | {ok, _} = net_kernel:start(?FUNCTION_NAME, #{name_domain => shortnames}),
104 | _ = cover:start(),
105 | [Node] = helpers:start_n_nodes(?FUNCTION_NAME, 1),
106 | ?assert(is_atom(Node)),
107 |
108 | Module = cover_compiled_mod2,
109 | Arg = arg,
110 | Ret = Module:run(Arg),
111 | ?assertEqual(Ret, erpc:call(Node, Module, run, [Arg])),
112 |
113 | Fun = fun Module:run/1,
114 | {_, _, ModFile} = code:get_object_code(Module),
115 | ?assertEqual(false, cover:is_compiled(Module)),
116 | InitialState = [{{Module, run, 1},
117 | {0, %% Covered
118 | 2 %% Not covered
119 | }}],
120 | Analysis = [{{Module, run, 1},
121 | {2, %% Covered
122 | 0 %% Not covered
123 | }}],
124 |
125 | %% We extract the regular module before we cover-compile it. We do this to
126 | %% compare the standalone functions to make sure that their module names
127 | %% are different because the code and the checksum are different.
128 | StandaloneFun1 = erpc:call(
129 | Node,
130 | horus, to_standalone_fun, [Fun, #{debug_info => true}]),
131 | ?assertEqual(Ret, erpc:call(Node, horus, exec, [StandaloneFun1, [Arg]])),
132 |
133 | %% We ensure that cover-compilation works and that we get the expected
134 | %% analysis results.
135 | %%
136 | %% The cover-compiled module is not available from `cover''s own state on
137 | %% the remote node, because it was loaded into the code server only.
138 | ?assertEqual({ok, Module}, cover:compile_beam(ModFile)),
139 | ?assertEqual(
140 | {error, not_main_node},
141 | erpc:call(Node, cover, is_compiled, [Module])),
142 | ?assertEqual({ok, InitialState}, cover:analyse(Module)),
143 |
144 | ?assertEqual(Ret, Module:run(Arg)),
145 | ?assertEqual({ok, Analysis}, cover:analyse(Module)),
146 |
147 | ok = cover:reset(Module),
148 |
149 | %% Now, we try to extract the cover-compiled module. We then execute the
150 | %% standalone function and verify the analysis again.
151 | ?assertEqual(
152 | {error, not_main_node},
153 | erpc:call(Node, cover, is_compiled, [Module])),
154 | ?assertEqual({ok, InitialState}, cover:analyse(Module)),
155 |
156 | StandaloneFun2 = erpc:call(
157 | Node,
158 | horus, to_standalone_fun, [Fun, #{debug_info => true}]),
159 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED(
160 | begin
161 | ?debugMsg(
162 | "Coverage support testing skipped as native coverage counters "
163 | "can't be modified externally")
164 | end,
165 | begin
166 | ?assertEqual(Ret, erpc:call(Node, horus, exec, [StandaloneFun2, [Arg]])),
167 | ?assertEqual({ok, Analysis}, cover:analyse(Module))
168 | end),
169 |
170 | ok = cover:reset(Module),
171 |
172 | %% We finally compare the standalone functions: they must have a different
173 | %% module name and checksum.
174 | GeneratedName1 = StandaloneFun1#horus_fun.module,
175 | GeneratedName2 = StandaloneFun2#horus_fun.module,
176 | ?assertNotEqual(GeneratedName1, GeneratedName2),
177 |
178 | Info1 = maps:get(fun_info, StandaloneFun1#horus_fun.debug_info),
179 | Info2 = maps:get(fun_info, StandaloneFun2#horus_fun.debug_info),
180 | ?assertEqual(Info1, Info2),
181 |
182 | Checksums1 = maps:get(checksums, StandaloneFun1#horus_fun.debug_info),
183 | Checksums2 = maps:get(checksums, StandaloneFun2#horus_fun.debug_info),
184 | ?assertNotEqual(
185 | maps:get(Module, Checksums1),
186 | maps:get(Module, Checksums2)),
187 |
188 | _ = net_kernel:stop(),
189 | ok.
190 |
--------------------------------------------------------------------------------
/test/cover_compiled_mod1.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(cover_compiled_mod1).
10 |
11 | -export([run/1]).
12 |
13 | run(Arg) ->
14 | Hash = erlang:phash2(Arg),
15 | {ok, Hash}.
16 |
--------------------------------------------------------------------------------
/test/cover_compiled_mod2.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(cover_compiled_mod2).
10 |
11 | -export([run/1]).
12 |
13 | run(Arg) ->
14 | Hash = erlang:phash2(Arg),
15 | {ok, Hash}.
16 |
--------------------------------------------------------------------------------
/test/erlang_binaries.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_binaries).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | -dialyzer([{no_match,
16 | [matches_type/2,
17 | trim_leading_dash3/2]}]).
18 |
19 | concat_binaries_test() ->
20 | Bin = helpers:ensure_not_optimized(<<"a">>),
21 | StandaloneFun = ?make_standalone_fun(
22 | <>),
23 | ?assertStandaloneFun(StandaloneFun),
24 | ?assertEqual(<<"a_a_a">>, horus:exec(StandaloneFun, [])).
25 |
26 | bs_match_test() ->
27 | List = [{'apply-to', <<"queues">>}],
28 | StandaloneFun = ?make_standalone_fun(
29 | begin
30 | matches_type(
31 | queue, proplists:get_value('apply-to', List)),
32 | ok
33 | end),
34 | ?assertStandaloneFun(StandaloneFun),
35 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
36 |
37 | matches_type(exchange, <<"exchanges">>) -> true;
38 | matches_type(queue, <<"queues">>) -> true;
39 | matches_type(exchange, <<"all">>) -> true;
40 | matches_type(queue, <<"all">>) -> true;
41 | matches_type(_, _) -> false.
42 |
43 | bitstring_init_test() ->
44 | StandaloneFun = ?make_standalone_fun(
45 | begin
46 | <<25:7/integer>> = encode_integer(25),
47 | ok
48 | end),
49 | ?assertStandaloneFun(StandaloneFun),
50 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
51 |
52 | encode_integer(Length) ->
53 | <>.
54 |
55 | bs_match_1_test() ->
56 | StandaloneFun = ?make_standalone_fun(
57 | begin
58 | {<<"2022">>, <<"02">>, <<"02">>} =
59 | parse_date(<<"2022-02-02">>),
60 | ok
61 | end),
62 | ?assertStandaloneFun(StandaloneFun),
63 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
64 |
65 | parse_date(
66 | <>) ->
67 | {Year, Month, Day}.
68 |
69 | bs_match_2_test() ->
70 | StandaloneFun = ?make_standalone_fun(
71 | begin
72 | {[], <<1, 2, 3, 4, 5>>} =
73 | parse_float(<<".", 1, 2, 3, 4, 5>>),
74 | ok
75 | end),
76 | ?assertStandaloneFun(StandaloneFun),
77 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
78 |
79 | parse_float(<<".", Rest/binary>>) ->
80 | parse_digits(Rest);
81 | parse_float(Bin) ->
82 | {[], Bin}.
83 |
84 | parse_digits(Bin) ->
85 | parse_digits(Bin, []).
86 |
87 | parse_digits(<>, Acc)
88 | when is_integer(Digit) andalso Digit >= 48 andalso Digit =< 57 ->
89 | parse_digits(Rest, [Digit | Acc]);
90 | parse_digits(Rest, Acc) ->
91 | {lists:reverse(Acc), Rest}.
92 |
93 | %% This set of parse_float, parse_digits, etc. is the same as the above
94 | %% functions and test case, except that the intermediary function
95 | %% `parse_digits/2' introduces new bindings that change the arity, to
96 | %% ensure we are not hard-coding an arity.
97 | parse_float2(<<".", Rest/binary>>) ->
98 | parse_digits2([], Rest);
99 | parse_float2(Bin) ->
100 | {[], Bin}.
101 |
102 | parse_digits2(Foo, Bin) ->
103 | parse_digits2(Foo, [], Bin).
104 |
105 | parse_digits2(Foo, Bar, Bin) ->
106 | parse_digits2(Foo, Bar, Bin, []).
107 |
108 | parse_digits2(Foo, Bar, <>, Acc)
109 | when is_integer(Digit) andalso Digit >= 48 andalso Digit =< 57 ->
110 | parse_digits2(Foo, Bar, Rest, [Digit | Acc]);
111 | parse_digits2(_Foo, _Bar, Rest, Acc) ->
112 | {lists:reverse(Acc), Rest}.
113 |
114 | bs_match_3_test() ->
115 | StandaloneFun = ?make_standalone_fun(
116 | begin
117 | {[], <<1, 2, 3, 4, 5>>} =
118 | parse_float2(<<".", 1, 2, 3, 4, 5>>),
119 | ok
120 | end),
121 | ?assertStandaloneFun(StandaloneFun),
122 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
123 |
124 | %% The compiler determines that this clause will always match because this
125 | %% function is not exported and is only called with a compile-time binary
126 | %% matching the pattern. As a result, the instruction for this match is
127 | %% `bs_start_match4'
128 | trim_leading_dash1(<<$-, Rest/binary>>) -> trim_leading_dash1(Rest);
129 | trim_leading_dash1(Binary) -> Binary.
130 |
131 | %% This is the same function but we'll give it a non-binary argument in
132 | %% the test case to avoid the `bs_start_match4' optimization. Instead
133 | %% the compiler uses a `{test,bs_start_match3,..}` instruction.
134 | trim_leading_dash2(<<$-, Rest/binary>>) -> trim_leading_dash2(Rest);
135 | trim_leading_dash2(Binary) -> Binary.
136 |
137 | %% Again, effectively the same function but to fix compilation for this
138 | %% case we need to determine the correct arity to mark as accepting
139 | %% a match context, so we should test a case where the binary match
140 | %% is done in another argument.
141 | trim_leading_dash3(Arg, <<$-, Rest/binary>>) -> trim_leading_dash3(Arg, Rest);
142 | trim_leading_dash3(_Arg, Binary) -> Binary.
143 |
144 | bs_match_accepts_match_context_test() ->
145 | StandaloneFun = ?make_standalone_fun(
146 | begin
147 | <<"5">> = trim_leading_dash1(<<"-5">>),
148 | <<"5">> = trim_leading_dash2(<<"-5">>),
149 | "-5" = trim_leading_dash2("-5"),
150 | "-5" = trim_leading_dash3([], "-5"),
151 | ok
152 | end),
153 | ?assertStandaloneFun(StandaloneFun),
154 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
155 |
156 | bs_get_float_test() ->
157 | FloatBin = helpers:ensure_not_optimized(<<3.14/float>>),
158 | StandaloneFun = ?make_standalone_fun(
159 | begin
160 | 3.14 = match_float(FloatBin),
161 | ok
162 | end),
163 | ?assertStandaloneFun(StandaloneFun),
164 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
165 |
166 | match_float(<>) ->
167 | Float.
168 |
169 | type_inference_for_test_arity_instruction_test() ->
170 | self() ! {text, false},
171 | TextFrame = receive TextMsg -> TextMsg end,
172 | self() ! {binary, true},
173 | BinaryFrame = receive BinaryMsg -> BinaryMsg end,
174 | StandaloneFun = ?make_standalone_fun(
175 | begin
176 | <<0:1>> = encode_frame(TextFrame),
177 | <<1:1>> = encode_frame(BinaryFrame),
178 | ok
179 | end),
180 | ?assertStandaloneFun(StandaloneFun),
181 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
182 |
183 | encode_frame(Frame)
184 | when is_tuple(Frame) andalso
185 | (element(1, Frame) =:= text orelse
186 | element(1, Frame) =:= binary) ->
187 | <<(encode_fin(Frame))/bitstring>>.
188 |
189 | encode_fin({text, false}) -> <<0:1/integer>>;
190 | encode_fin({binary, false}) -> <<0:1/integer>>;
191 | encode_fin(_) -> <<1:1/integer>>.
192 |
193 | bit_string_comprehension_expression_test() ->
194 | Data = crypto:strong_rand_bytes(128),
195 | <> = crypto:strong_rand_bytes(4),
196 | StandaloneFun = ?make_standalone_fun(
197 | begin
198 | <<<<(Part bxor Mask):32/integer>>
199 | || <> <= Data>>
200 | end),
201 | ?assertStandaloneFun(StandaloneFun),
202 | ?assertEqual(
203 | <<<<(Part bxor Mask):32/integer>>
204 | || <> <= Data>>,
205 | horus:exec(StandaloneFun, [])).
206 |
207 | bitstring_flags_test() ->
208 | LittleSignedBin = helpers:ensure_not_optimized(
209 | <<-42:4/little-signed-integer-unit:8>>),
210 | LittleUnsignedBin = helpers:ensure_not_optimized(
211 | <<42:4/little-unsigned-integer-unit:8>>),
212 | BigSignedBin = helpers:ensure_not_optimized(
213 | <<-42:4/big-signed-integer-unit:8>>),
214 | BigUnsignedBin = helpers:ensure_not_optimized(
215 | <<42:4/big-unsigned-integer-unit:8>>),
216 | Decode = ?make_standalone_fun(
217 | begin
218 | {match_bitstring_flags(
219 | {little_signed, LittleSignedBin}),
220 | match_bitstring_flags(
221 | {little_unsigned, LittleUnsignedBin}),
222 | match_bitstring_flags(
223 | {big_signed, BigSignedBin}),
224 | match_bitstring_flags(
225 | {big_unsigned, BigUnsignedBin})}
226 | end),
227 | ?assertStandaloneFun(Decode),
228 | ?assertEqual({-42, 42, -42, 42}, horus:exec(Decode, [])).
229 |
230 | match_bitstring_flags(
231 | {little_signed, <>}) ->
232 | N;
233 | match_bitstring_flags(
234 | {big_signed, <>}) ->
235 | N;
236 | match_bitstring_flags(
237 | {little_unsigned, <>}) ->
238 | N;
239 | match_bitstring_flags(
240 | {big_unsigned, <>}) ->
241 | N.
242 |
--------------------------------------------------------------------------------
/test/erlang_blocks.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_blocks).
10 |
11 | -if(?OTP_RELEASE >= 25).
12 | -feature(maybe_expr,enable).
13 | -endif.
14 |
15 | -include_lib("eunit/include/eunit.hrl").
16 |
17 | -include("test/helpers.hrl").
18 |
19 | begin_test() ->
20 | StandaloneFun = ?make_standalone_fun(
21 | begin
22 | ok
23 | end),
24 | ?assertStandaloneFun(StandaloneFun),
25 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
26 |
27 | case_test() ->
28 | StandaloneFun = ?make_standalone_fun(
29 | case helpers:ensure_not_optimized({a, b}) of
30 | {_} -> error;
31 | {_, _} -> ok;
32 | {_, _, _} -> error
33 | end),
34 | ?assertStandaloneFun(StandaloneFun),
35 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
36 |
37 | if_test() ->
38 | StandaloneFun = ?make_standalone_fun(
39 | begin
40 | Ret = helpers:ensure_not_optimized(a),
41 | if
42 | Ret =:= a -> ok;
43 | true -> error
44 | end
45 | end),
46 | ?assertStandaloneFun(StandaloneFun),
47 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
48 |
49 | receive_any_test() ->
50 | self() ! ?FUNCTION_NAME,
51 | StandaloneFun = ?make_standalone_fun(
52 | begin
53 | receive
54 | Msg -> Msg
55 | end
56 | end),
57 | ?assertStandaloneFun(StandaloneFun),
58 | ?assertEqual(?FUNCTION_NAME, horus:exec(StandaloneFun, [])).
59 |
60 | receive_match_test() ->
61 | self() ! ?FUNCTION_NAME,
62 | StandaloneFun = ?make_standalone_fun(
63 | begin
64 | receive
65 | ?FUNCTION_NAME -> ok;
66 | _ -> error
67 | end
68 | end),
69 | ?assertStandaloneFun(StandaloneFun),
70 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
71 |
72 | receive_after_test() ->
73 | self() ! ?FUNCTION_NAME,
74 | StandaloneFun = ?make_standalone_fun(
75 | begin
76 | receive
77 | Msg -> Msg
78 | after 10000 -> error
79 | end
80 | end),
81 | ?assertStandaloneFun(StandaloneFun),
82 | ?assertEqual(?FUNCTION_NAME, horus:exec(StandaloneFun, [])).
83 |
84 | try_catch_test() ->
85 | StandaloneFun = ?make_standalone_fun(
86 | try
87 | 0 = helpers:ensure_not_optimized(1),
88 | error
89 | catch
90 | _:_ ->
91 | ok
92 | end),
93 | ?assertStandaloneFun(StandaloneFun),
94 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
95 |
96 | try_catch_after_test() ->
97 | StandaloneFun = ?make_standalone_fun(
98 | try
99 | 0 = helpers:ensure_not_optimized(1),
100 | error
101 | catch
102 | _:_ ->
103 | ok
104 | after
105 | nothing_returned
106 | end),
107 | ?assertStandaloneFun(StandaloneFun),
108 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
109 |
110 | try_of_test() ->
111 | StandaloneFun = ?make_standalone_fun(
112 | try helpers:ensure_not_optimized(1) of
113 | 0 -> error;
114 | 1 -> ok;
115 | 2 -> error
116 | catch
117 | _:_ ->
118 | ok
119 | end),
120 | ?assertStandaloneFun(StandaloneFun),
121 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
122 |
123 | catch_test() ->
124 | StandaloneFun = ?make_standalone_fun(
125 | begin
126 | _ = catch (0 = helpers:ensure_not_optimized(1)),
127 | ok
128 | end),
129 | ?assertStandaloneFun(StandaloneFun),
130 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
131 |
132 | raise_test() ->
133 | StandaloneFun = ?make_standalone_fun(
134 | try
135 | 0 = helpers:ensure_not_optimized(1),
136 | error
137 | catch
138 | Class:Reason:Stacktrace ->
139 | erlang:raise(Class, Reason, Stacktrace)
140 | end),
141 | ?assertStandaloneFun(StandaloneFun),
142 | ?assertError({badmatch, 1}, horus:exec(StandaloneFun, [])).
143 |
144 | -if(?OTP_RELEASE >= 25).
145 | -if(?FEATURE_ENABLED(maybe_expr)).
146 | maybe_test() ->
147 | StandaloneFun = ?make_standalone_fun(
148 | begin
149 | maybe
150 | {ok, A} ?= erlang:list_to_tuple([ok, 42]),
151 | true = A >= 0,
152 | {ok, B} ?= erlang:list_to_tuple([ok, 58]),
153 | A + B
154 | end
155 | end),
156 | ?assertStandaloneFun(StandaloneFun),
157 | ?assertEqual(100, horus:exec(StandaloneFun, [])).
158 |
159 | maybe_else_test() ->
160 | StandaloneFun = ?make_standalone_fun(
161 | begin
162 | maybe
163 | {ok, A} ?= erlang:list_to_tuple([ok, 42]),
164 | true = A >= 0,
165 | {ok, B} ?= receive
166 | {not_receiving, Msg} -> Msg
167 | after 0 ->
168 | {error, reason}
169 | end,
170 | A + B
171 | else
172 | {error, Reason} ->
173 | Reason
174 | end
175 | end),
176 | ?assertStandaloneFun(StandaloneFun),
177 | ?assertEqual(reason, horus:exec(StandaloneFun, [])).
178 | -endif.
179 | -endif.
180 |
--------------------------------------------------------------------------------
/test/erlang_builtins.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_builtins).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | node_bif_test() ->
16 | StandaloneFun = ?make_standalone_fun(node()),
17 | ?assertStandaloneFun(StandaloneFun),
18 | ?assertEqual(node(), horus:exec(StandaloneFun, [])).
19 |
20 | self_bif_test() ->
21 | StandaloneFun = ?make_standalone_fun(self()),
22 | ?assertStandaloneFun(StandaloneFun),
23 | ?assertEqual(self(), horus:exec(StandaloneFun, [])).
24 |
25 | apply_test() ->
26 | Module = helpers:ensure_not_optimized(erlang),
27 | StandaloneFun = ?make_standalone_fun(
28 | begin
29 | c = hd(Module:tl([[a, b], c])),
30 | ok
31 | end),
32 | ?assertStandaloneFun(StandaloneFun),
33 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
34 |
35 | %% `apply_last' instruction is used when the apply is the last call
36 | %% in the function.
37 | apply_last_test() ->
38 | Module = helpers:ensure_not_optimized(erlang),
39 | StandaloneFun = ?make_standalone_fun(
40 | begin
41 | Module:tl([[a, b], c])
42 | end),
43 | ?assertStandaloneFun(StandaloneFun),
44 | ?assertEqual([c], horus:exec(StandaloneFun, [])).
45 |
--------------------------------------------------------------------------------
/test/erlang_exprs.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_exprs).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | add_test() ->
16 | Three = helpers:ensure_not_optimized(3),
17 | StandaloneFun = ?make_standalone_fun(Three + 2),
18 | ?assertStandaloneFun(StandaloneFun),
19 | ?assertEqual(5, horus:exec(StandaloneFun, [])).
20 |
21 | subtract_test() ->
22 | Three = helpers:ensure_not_optimized(3),
23 | StandaloneFun = ?make_standalone_fun(Three - 2),
24 | ?assertStandaloneFun(StandaloneFun),
25 | ?assertEqual(1, horus:exec(StandaloneFun, [])).
26 |
27 | multiply_test() ->
28 | Three = helpers:ensure_not_optimized(3),
29 | StandaloneFun = ?make_standalone_fun(Three * 2),
30 | ?assertStandaloneFun(StandaloneFun),
31 | ?assertEqual(6, horus:exec(StandaloneFun, [])).
32 |
33 | divide_test() ->
34 | Three = helpers:ensure_not_optimized(3),
35 | StandaloneFun = ?make_standalone_fun(Three / 2),
36 | ?assertStandaloneFun(StandaloneFun),
37 | ?assertEqual(1.5, horus:exec(StandaloneFun, [])).
38 |
39 | integer_divide_test() ->
40 | Three = helpers:ensure_not_optimized(3),
41 | StandaloneFun = ?make_standalone_fun(Three div 2),
42 | ?assertStandaloneFun(StandaloneFun),
43 | ?assertEqual(1, horus:exec(StandaloneFun, [])).
44 |
45 | remainder_test() ->
46 | Three = helpers:ensure_not_optimized(3),
47 | StandaloneFun = ?make_standalone_fun(Three rem 2),
48 | ?assertStandaloneFun(StandaloneFun),
49 | ?assertEqual(1, horus:exec(StandaloneFun, [])).
50 |
51 | lt_test() ->
52 | Three = helpers:ensure_not_optimized(3),
53 | StandaloneFun = ?make_standalone_fun(Three < 2),
54 | ?assertStandaloneFun(StandaloneFun),
55 | ?assertEqual(false, horus:exec(StandaloneFun, [])).
56 |
57 | le_test() ->
58 | Three = helpers:ensure_not_optimized(3),
59 | StandaloneFun = ?make_standalone_fun(Three =< 2),
60 | ?assertStandaloneFun(StandaloneFun),
61 | ?assertEqual(false, horus:exec(StandaloneFun, [])).
62 |
63 | gt_test() ->
64 | Three = helpers:ensure_not_optimized(3),
65 | StandaloneFun = ?make_standalone_fun(Three > 2),
66 | ?assertStandaloneFun(StandaloneFun),
67 | ?assertEqual(true, horus:exec(StandaloneFun, [])).
68 |
69 | ge_test() ->
70 | Three = helpers:ensure_not_optimized(3),
71 | StandaloneFun = ?make_standalone_fun(Three >= 2),
72 | ?assertStandaloneFun(StandaloneFun),
73 | ?assertEqual(true, horus:exec(StandaloneFun, [])).
74 |
75 | binary_and_test() ->
76 | One = helpers:ensure_not_optimized(1),
77 | StandaloneFun = ?make_standalone_fun(One band 2),
78 | ?assertStandaloneFun(StandaloneFun),
79 | ?assertEqual(0, horus:exec(StandaloneFun, [])).
80 |
81 | binary_or_test() ->
82 | One = helpers:ensure_not_optimized(1),
83 | StandaloneFun = ?make_standalone_fun(One bor 2),
84 | ?assertStandaloneFun(StandaloneFun),
85 | ?assertEqual(3, horus:exec(StandaloneFun, [])).
86 |
87 | binary_lshift_test() ->
88 | One = helpers:ensure_not_optimized(1),
89 | StandaloneFun = ?make_standalone_fun(One bsl 1),
90 | ?assertStandaloneFun(StandaloneFun),
91 | ?assertEqual(2, horus:exec(StandaloneFun, [])).
92 |
93 | binary_rshift_test() ->
94 | Two = helpers:ensure_not_optimized(2),
95 | StandaloneFun = ?make_standalone_fun(Two bsr 1),
96 | ?assertStandaloneFun(StandaloneFun),
97 | ?assertEqual(1, horus:exec(StandaloneFun, [])).
98 |
99 | logical_and_test() ->
100 | True = helpers:ensure_not_optimized(true),
101 | StandaloneFun = ?make_standalone_fun(True and false),
102 | ?assertStandaloneFun(StandaloneFun),
103 | ?assertEqual(false, horus:exec(StandaloneFun, [])).
104 |
105 | logical_andalso_test() ->
106 | True = helpers:ensure_not_optimized(true),
107 | StandaloneFun = ?make_standalone_fun(True andalso false),
108 | ?assertStandaloneFun(StandaloneFun),
109 | ?assertEqual(false, horus:exec(StandaloneFun, [])).
110 |
111 | logical_or_test() ->
112 | True = helpers:ensure_not_optimized(true),
113 | StandaloneFun = ?make_standalone_fun(True or false),
114 | ?assertStandaloneFun(StandaloneFun),
115 | ?assertEqual(true, horus:exec(StandaloneFun, [])).
116 |
117 | logical_orelse_test() ->
118 | True = helpers:ensure_not_optimized(true),
119 | StandaloneFun = ?make_standalone_fun(True orelse false),
120 | ?assertStandaloneFun(StandaloneFun),
121 | ?assertEqual(true, horus:exec(StandaloneFun, [])).
122 |
123 | logical_xor_test() ->
124 | True = helpers:ensure_not_optimized(true),
125 | StandaloneFun = ?make_standalone_fun(True xor false),
126 | ?assertStandaloneFun(StandaloneFun),
127 | ?assertEqual(true, horus:exec(StandaloneFun, [])).
128 |
129 | send_message_test() ->
130 | StandaloneFun = ?make_standalone_fun(
131 | begin
132 | self() ! message,
133 | receive Msg -> Msg end
134 | end),
135 | ?assertStandaloneFun(StandaloneFun),
136 | ?assertEqual(message, horus:exec(StandaloneFun, [])).
137 |
--------------------------------------------------------------------------------
/test/erlang_lists.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_lists).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | list_prepend_test() ->
16 | List = helpers:ensure_not_optimized([b]),
17 | StandaloneFun = ?make_standalone_fun([a | List]),
18 | ?assertStandaloneFun(StandaloneFun),
19 | ?assertEqual([a, b], horus:exec(StandaloneFun, [])).
20 |
21 | list_concat_test() ->
22 | List = helpers:ensure_not_optimized([a]),
23 | StandaloneFun = ?make_standalone_fun(List ++ [b]),
24 | ?assertStandaloneFun(StandaloneFun),
25 | ?assertEqual([a, b], horus:exec(StandaloneFun, [])).
26 |
27 | list_diff_test() ->
28 | List = helpers:ensure_not_optimized([a, b]),
29 | StandaloneFun = ?make_standalone_fun(List -- [b]),
30 | ?assertStandaloneFun(StandaloneFun),
31 | ?assertEqual([a], horus:exec(StandaloneFun, [])).
32 |
33 | list_comprehension_test() ->
34 | StandaloneFun = ?make_standalone_fun(
35 | begin
36 | [erlang:abs(I) || I <- [1, 2, 3]]
37 | end),
38 | ?assertStandaloneFun(StandaloneFun),
39 | ?assertEqual([1, 2, 3], horus:exec(StandaloneFun, [])).
40 |
41 | list_comprehension_with_conditions_test() ->
42 | StandaloneFun = ?make_standalone_fun(
43 | begin
44 | [erlang:abs(I)
45 | || I <- [1, 2, 3],
46 | I >= 2]
47 | end),
48 | ?assertStandaloneFun(StandaloneFun),
49 | ?assertEqual([2, 3], horus:exec(StandaloneFun, [])).
50 |
51 | list_comprehension_with_multiple_qualifiers_test() ->
52 | StandaloneFun = ?make_standalone_fun(
53 | begin
54 | [Value
55 | || Props <- [[{a, 1}],
56 | [{b, 2}],
57 | [{c, 3}]],
58 | {_, Value} <- Props]
59 | end),
60 | ?assertStandaloneFun(StandaloneFun),
61 | ?assertEqual([1, 2, 3], horus:exec(StandaloneFun, [])).
62 |
63 | list_pattern_matching_test() ->
64 | List = helpers:ensure_not_optimized([a, b, c]),
65 |
66 | %% Tests the get_list/3 instruction.
67 | Reverse = ?make_standalone_fun(reverse(List)),
68 | ?assertStandaloneFun(Reverse),
69 | ?assertEqual([c, b, a], horus:exec(Reverse, [])),
70 |
71 | %% Tests the get_hd/2 instruction.
72 | ReverseHead = ?make_standalone_fun(
73 | begin
74 | [Head | _] = reverse(List),
75 | Head
76 | end),
77 | ?assertStandaloneFun(ReverseHead),
78 | ?assertEqual(c, horus:exec(ReverseHead, [])).
79 |
80 | reverse(List) ->
81 | reverse(List, []).
82 |
83 | reverse([Head | Tail], Acc) ->
84 | reverse(Tail, [Head | Acc]);
85 | reverse([], Acc) ->
86 | Acc.
87 |
--------------------------------------------------------------------------------
/test/erlang_literals.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_literals).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | atom_test() ->
16 | StandaloneFun = ?make_standalone_fun(ok),
17 | ?assertStandaloneFun(StandaloneFun),
18 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
19 |
20 | small_int_test() ->
21 | StandaloneFun = ?make_standalone_fun(3),
22 | ?assertStandaloneFun(StandaloneFun),
23 | ?assertEqual(3, horus:exec(StandaloneFun, [])).
24 |
25 | large_int_test() ->
26 | StandaloneFun = ?make_standalone_fun(576460752303423489),
27 | ?assertStandaloneFun(StandaloneFun),
28 | ?assertEqual(576460752303423489, horus:exec(StandaloneFun, [])).
29 |
30 | float_test() ->
31 | StandaloneFun = ?make_standalone_fun(3.0),
32 | ?assertStandaloneFun(StandaloneFun),
33 | ?assertEqual(3.0, horus:exec(StandaloneFun, [])).
34 |
35 | binary_test() ->
36 | StandaloneFun = ?make_standalone_fun(<<"3">>),
37 | ?assertStandaloneFun(StandaloneFun),
38 | ?assertEqual(<<"3">>, horus:exec(StandaloneFun, [])).
39 |
40 | list_test() ->
41 | StandaloneFun = ?make_standalone_fun([a, b]),
42 | ?assertStandaloneFun(StandaloneFun),
43 | ?assertEqual([a, b], horus:exec(StandaloneFun, [])).
44 |
45 | map_test() ->
46 | StandaloneFun = ?make_standalone_fun(#{a => b}),
47 | ?assertStandaloneFun(StandaloneFun),
48 | ?assertEqual(#{a => b}, horus:exec(StandaloneFun, [])).
49 |
--------------------------------------------------------------------------------
/test/erlang_maps.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_maps).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | match_test() ->
16 | StandaloneFun = ?make_standalone_fun(
17 | begin
18 | Map = #{a => b},
19 | #{a := Value} = Map,
20 | Value
21 | end),
22 | ?assertStandaloneFun(StandaloneFun),
23 | ?assertEqual(b, horus:exec(StandaloneFun, [])).
24 |
25 | update_test() ->
26 | StandaloneFun = ?make_standalone_fun(
27 | begin
28 | Map = #{a => b},
29 | Map#{a => c}
30 | end),
31 | ?assertStandaloneFun(StandaloneFun),
32 | ?assertEqual(#{a => c}, horus:exec(StandaloneFun, [])).
33 |
--------------------------------------------------------------------------------
/test/erlang_records.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_records).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | -record(my_record, {field}).
16 | -record(pair, {a, b}).
17 |
18 | create_record_test() ->
19 | Field = helpers:ensure_not_optimized(ok),
20 | StandaloneFun = ?make_standalone_fun(#my_record{field = Field}),
21 | ?assertStandaloneFun(StandaloneFun),
22 | ?assertEqual(#my_record{field = Field}, horus:exec(StandaloneFun, [])).
23 |
24 | update_record_test() ->
25 | Record = helpers:ensure_not_optimized(#pair{a = 123, b = 456}),
26 | StandaloneFun = ?make_standalone_fun(Record#pair{a = 789}),
27 | ?assertStandaloneFun(StandaloneFun),
28 | ?assertEqual(#pair{a = 789, b = 456}, horus:exec(StandaloneFun, [])).
29 |
30 | match_record_test() ->
31 | Record = helpers:ensure_not_optimized(#my_record{field = ok}),
32 | StandaloneFun = ?make_standalone_fun(
33 | begin
34 | #my_record{field = Field} = Record,
35 | Field
36 | end),
37 | ?assertStandaloneFun(StandaloneFun),
38 | ?assertEqual(ok, horus:exec(StandaloneFun, [])).
39 |
--------------------------------------------------------------------------------
/test/erlang_tuples.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(erlang_tuples).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | select_tuple_arity_var_info_test() ->
16 | Nodes = [node() | nodes()],
17 | StandaloneFun = ?make_standalone_fun(
18 | begin
19 | Tuple = make_tuple(Nodes),
20 | handle_tuple(Tuple)
21 | end),
22 | ?assertStandaloneFun(StandaloneFun),
23 | ?assertEqual(true, horus:exec(StandaloneFun, [])).
24 |
25 | make_tuple([A]) ->
26 | {a, A};
27 | make_tuple([A, B]) ->
28 | {b, A, B};
29 | make_tuple([_, B, C]) ->
30 | {c, B, C}.
31 |
32 | handle_tuple(Tuple) when is_tuple(Tuple) ->
33 | case Tuple of
34 | {a, _} -> true;
35 | {b, _, _} -> false;
36 | {c, _, _} -> false
37 | end.
38 |
--------------------------------------------------------------------------------
/test/failing_funs.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(failing_funs).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | -dialyzer([{no_return,
16 | [stacktrace_test/0]}]).
17 |
18 | throw_test() ->
19 | StandaloneFun = ?make_standalone_fun(
20 | begin
21 | throw(failure)
22 | end),
23 | ?assertStandaloneFun(StandaloneFun),
24 | ?assertThrow(failure, horus:exec(StandaloneFun, [])).
25 |
26 | error_test() ->
27 | StandaloneFun = ?make_standalone_fun(
28 | begin
29 | error(failure)
30 | end),
31 | ?assertStandaloneFun(StandaloneFun),
32 | ?assertError(failure, horus:exec(StandaloneFun, [])).
33 |
34 | exit_test() ->
35 | StandaloneFun = ?make_standalone_fun(
36 | begin
37 | exit(failure)
38 | end),
39 | ?assertStandaloneFun(StandaloneFun),
40 | ?assertExit(failure, horus:exec(StandaloneFun, [])).
41 |
42 | stacktrace_test() ->
43 | Fun = fun() -> failing_fun() end,
44 | StandaloneFun = horus:to_standalone_fun(Fun, #{debug_info => true}),
45 | ?assertStandaloneFun(StandaloneFun),
46 | Stacktrace1 = try
47 | Fun()
48 | catch
49 | error:failure:Stacktrace ->
50 | Stacktrace
51 | end,
52 | try
53 | horus:exec(StandaloneFun, [])
54 | catch
55 | error:failure:Stacktrace2 ->
56 | %% When comparing the stacktraces, we only consider the part
57 | %% inside the anonymous function. That's why we take all frames
58 | %% while they belong to the anonymous function and the function is
59 | %% calls.
60 | %%
61 | %% For `Stacktrace1', we will stop before the frame calling the
62 | %% anonymous function (i.e. this test function). For
63 | %% `Stacktrace2', we will stop before the `horus:exec/2' call.
64 | Pred = fun({Module, Name, _, _}) ->
65 | Module =:= ?MODULE andalso
66 | Name =/= ?FUNCTION_NAME
67 | end,
68 | Stacktrace1a = lists:takewhile(Pred, Stacktrace1),
69 | Stacktrace2a = lists:takewhile(Pred, Stacktrace2),
70 | ?assertNotEqual([], Stacktrace1a),
71 | ?assertNotEqual([], Stacktrace2a),
72 | ?assertEqual(Stacktrace1a, Stacktrace2a)
73 | end.
74 |
75 | -spec failing_fun() -> no_return().
76 |
77 | failing_fun() ->
78 | error(failure).
79 |
--------------------------------------------------------------------------------
/test/fun_env.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(fun_env).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("src/horus_fun.hrl").
14 |
15 | -include("test/helpers.hrl").
16 |
17 | list_in_fun_env_test() ->
18 | List = make_list(),
19 | StandaloneFun = horus:to_standalone_fun(fun() -> List end),
20 | ?assertStandaloneFun(StandaloneFun),
21 | ?assertEqual(List, horus:exec(StandaloneFun, [])).
22 |
23 | make_list() -> [a, b].
24 |
25 | map_in_fun_env_test() ->
26 | Map = make_map(),
27 | StandaloneFun = horus:to_standalone_fun(fun() -> Map end),
28 | ?assertStandaloneFun(StandaloneFun),
29 | ?assertEqual(Map, horus:exec(StandaloneFun, [])).
30 |
31 | make_map() -> #{a => b}.
32 |
33 | tuple_in_fun_env_test() ->
34 | Tuple = make_tuple(),
35 | StandaloneFun = horus:to_standalone_fun(fun() -> Tuple end),
36 | ?assertStandaloneFun(StandaloneFun),
37 | ?assertEqual(Tuple, horus:exec(StandaloneFun, [])).
38 |
39 | make_tuple() -> {a, b}.
40 |
41 | binary_in_fun_env_test() ->
42 | Binary = make_binary(),
43 | StandaloneFun = horus:to_standalone_fun(fun() -> Binary end),
44 | ?assertStandaloneFun(StandaloneFun),
45 | ?assertEqual(Binary, horus:exec(StandaloneFun, [])).
46 |
47 | make_binary() -> <<"ab">>.
48 |
--------------------------------------------------------------------------------
/test/fun_extraction_SUITE.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2022-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(fun_extraction_SUITE).
10 |
11 | -include_lib("stdlib/include/assert.hrl").
12 | -include_lib("eunit/include/eunit.hrl").
13 |
14 | -export([all/0,
15 | groups/0,
16 | init_per_suite/1,
17 | end_per_suite/1,
18 | init_per_group/2,
19 | end_per_group/2,
20 | init_per_testcase/2,
21 | end_per_testcase/2,
22 |
23 | fun_extraction_works_in_rebar3_ct/1]).
24 |
25 | all() ->
26 | [fun_extraction_works_in_rebar3_ct].
27 |
28 | groups() ->
29 | [].
30 |
31 | init_per_suite(Config) ->
32 | ok = cth_log_redirect:handle_remote_events(true),
33 | Config.
34 |
35 | end_per_suite(_Config) ->
36 | ok.
37 |
38 | init_per_group(_Group, Config) ->
39 | Config.
40 |
41 | end_per_group(_Group, _Config) ->
42 | ok.
43 |
44 | init_per_testcase(fun_extraction_works_in_rebar3_ct, Config) ->
45 | ModifiedMods = code:modified_modules(),
46 | HorusFunModified = lists:member(horus, ModifiedMods),
47 | ct:pal(
48 | "Modified modules: ~p~n"
49 | "`horus` recompiled: ~s",
50 | [ModifiedMods, HorusFunModified]),
51 | case HorusFunModified of
52 | true ->
53 | Config;
54 | false ->
55 | {skip,
56 | "Horus not recompiled at runtime by Rebar; "
57 | "test conditions not met"}
58 | end;
59 | init_per_testcase(_Testcase, Config) ->
60 | Config.
61 |
62 | end_per_testcase(_Testcase, _Config) ->
63 | ok.
64 |
65 | fun_extraction_works_in_rebar3_ct(_Config) ->
66 | %% Rebar3 uses `cth_readable' to offer a cleaner output of common_test on
67 | %% stdout. As part of that, it recompiles the entire application on-the-fly
68 | %% with the `cth_readable_transform' parse_transform. This means that the
69 | %% module on disk and the one loaded in memory are different. See
70 | %% `horus:is_recompiled_module_acceptable()'.
71 | %%
72 | %% This testcase depends on that behavior (see `init_per_testcase').
73 | %%
74 | %% To test `horus', it needs to get an anonymous function created by
75 | %% the application (not the testsuite) as the top-level function to
76 | %% extract.
77 | %%
78 | %% The `InnerFun' is just here to have an argument to pass to
79 | %% `horus:to_actual_arg()'. `horus:to_actual_arg()' is the
80 | %% function we call to get that anonymous function from the application.
81 | InnerFun = fun() -> true end,
82 | InnerStandaloneFun = horus:to_standalone_fun(InnerFun),
83 | Options = #{ensure_instruction_is_permitted =>
84 | fun(_) -> ok end,
85 | should_process_function =>
86 | fun
87 | (code, _, _, _) -> false;
88 | (erlang, _, _, _) -> false;
89 | (lists, _, _, _) -> false;
90 | (maps, _, _, _) -> false;
91 | (horus, exec, _, _) -> false;
92 | (_Mod, _, _, _) -> true
93 | end},
94 | OuterFun = horus:to_actual_arg(InnerStandaloneFun),
95 |
96 | %% Without `horus:is_recompiled_module_acceptable()', `horus'
97 | %% would fail the extraction below because of the two copies of the
98 | %% `horus' module.
99 | OuterStandaloneFun = horus:to_standalone_fun(OuterFun, Options),
100 |
101 | %% We execute the extracted function just as an extra check, but this is
102 | %% not critical for this testcase.
103 | ?assertEqual(
104 | OuterFun(),
105 | horus:exec(OuterStandaloneFun, [])),
106 |
107 | ok.
108 |
--------------------------------------------------------------------------------
/test/helpers.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(helpers).
10 |
11 | -export([ensure_not_optimized/1,
12 | start_epmd/0,
13 | start_n_nodes/2]).
14 |
15 | -spec ensure_not_optimized(Value) -> Value when
16 | Value :: term().
17 | %% @doc Makes sure the given value is not optimized out by the compiler.
18 | %%
19 | %% The compiler is smart enough to optimize away many instructions by
20 | %% inspecting types and values. This function confuses the compiler by sending
21 | %% and receiving the value.
22 |
23 | ensure_not_optimized(Value) ->
24 | self() ! Value,
25 | receive Msg -> Msg end.
26 |
27 | start_epmd() ->
28 | RootDir = code:root_dir(),
29 | ErtsVersion = erlang:system_info(version),
30 | ErtsDir = lists:flatten(io_lib:format("erts-~ts", [ErtsVersion])),
31 | EpmdPath0 = filename:join([RootDir, ErtsDir, "bin", "epmd"]),
32 | EpmdPath = case os:type() of
33 | {win32, _} -> EpmdPath0 ++ ".exe";
34 | _ -> EpmdPath0
35 | end,
36 | Port = erlang:open_port(
37 | {spawn_executable, EpmdPath},
38 | [{args, ["-daemon"]}]),
39 | erlang:port_close(Port),
40 | ok.
41 |
42 | start_n_nodes(NamePrefix, Count) ->
43 | io:format("Start ~b Erlang nodes:~n", [Count]),
44 | Nodes = [begin
45 | Name = lists:flatten(
46 | io_lib:format(
47 | "~s-~s-~b", [?MODULE, NamePrefix, I])),
48 | io:format("- ~s~n", [Name]),
49 | start_erlang_node(Name)
50 | end || I <- lists:seq(1, Count)],
51 | io:format("Started nodes: ~p~n", [[Node || {Node, _Peer} <- Nodes]]),
52 |
53 | CodePath = code:get_path(),
54 | lists:foreach(
55 | fun({Node, _Peer}) ->
56 | erpc:call(Node, code, add_pathsz, [CodePath])
57 | end, Nodes),
58 |
59 | %% We add all nodes to the test coverage report.
60 | CoveredNodes = [Node || {Node, _Peer} <- Nodes],
61 | {ok, _} = cover:start(CoveredNodes),
62 |
63 | CoveredNodes.
64 |
65 | -if(?OTP_RELEASE >= 25).
66 | start_erlang_node(Name) ->
67 | Name1 = list_to_atom(Name),
68 | {ok, Peer, Node} = peer:start(#{name => Name1,
69 | wait_boot => infinity}),
70 | {Node, Peer}.
71 | -else.
72 | start_erlang_node(Name) ->
73 | Name1 = list_to_atom(Name),
74 | Options = [{monitor_master, true}],
75 | {ok, Node} = ct_slave:start(Name1, Options),
76 | {Node, Node}.
77 | -endif.
78 |
--------------------------------------------------------------------------------
/test/helpers.hrl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -include_lib("eunit/include/eunit.hrl").
10 |
11 | -include("src/horus_fun.hrl").
12 |
13 | -define(make_standalone_fun(Expression),
14 | fun() ->
15 | __Fun = fun() -> Expression end,
16 | horus:to_standalone_fun(__Fun, #{debug_info => true})
17 | end()).
18 |
19 | -define(assertStandaloneFun(StandaloneFun),
20 | ?assertMatch(#horus_fun{}, StandaloneFun)).
21 |
22 | -define(assertToFunError(Expected, Expression),
23 | ?assertError(Expected, ?make_standalone_fun(Expression))).
24 |
--------------------------------------------------------------------------------
/test/is_module_loaded.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(is_module_loaded).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | on_a_loaded_module_test() ->
14 | _ = lists:module_info(),
15 | ?assert(horus_utils:is_module_loaded(lists)).
16 |
17 | on_a_missing_module_test() ->
18 | Module = 'non_existing_module!',
19 | ?assertError(undef, Module:module_info()),
20 | ?assertNot(horus_utils:is_module_loaded(Module)).
21 |
--------------------------------------------------------------------------------
/test/line_chunk.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2022-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(line_chunk).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | decode_line_chunk_test() ->
14 | %% Taken from `rabbit_amqqueue'.
15 | Module = rabbit_amqqueue,
16 | Chunk = <<0,0,0,0,0,0,0,0,0,0,4,187,0,0,3,242,0,0,0,0,9,122,9,
17 | 123,9,124,9,128,9,132,9,130,9,140,9,141,9,142,9,144,
18 | 9,145,9,147,9,152,9,153,9,156,9,158,9,159,9,160,9,
19 | 161,9,165,9,171,9,176,9,177,9,189,9,190,9,191,9,198,
20 | 9,199,9,200,9,207,9,208,9,216,9,217,9,227,9,228,9,
21 | 229,9,230,9,237,9,238,9,246,9,247,9,253,9,254,9,255,
22 | 41,0,41,17,41,35,41,37,41,38,41,39,41,41,41,50,41,
23 | 55,41,58,41,59,41,63,41,61,41,69,41,71,41,73,41,77,
24 | 41,78,41,86,41,94,41,95,41,101,41,102,41,103,41,104,
25 | 41,110,41,111,41,112,41,113,41,115,41,120,41,121,41,
26 | 122,41,124,41,127,41,128,41,129,41,131,41,134,41,
27 | 135,41,136,41,152,41,153,41,154,41,155,41,156,41,
28 | 157,41,158,41,159,41,180,41,181,41,185,41,190,41,
29 | 191,41,203,41,204,41,206,41,207,41,208,41,210,41,
30 | 218,41,219,41,220,41,222,41,223,41,224,41,227,41,
31 | 228,41,239,41,241,41,243,41,247,41,248,41,252,41,
32 | 253,73,3,73,8,73,9,73,10,73,15,73,16,73,18,73,19,73,
33 | 23,73,24,73,28,73,29,73,38,73,39,73,43,73,45,73,49,
34 | 73,65,73,66,73,67,73,68,73,69,73,70,73,71,73,76,73,
35 | 77,73,79,73,81,73,87,73,88,73,89,73,98,73,103,73,
36 | 104,73,108,73,112,73,114,73,116,73,117,73,122,73,
37 | 127,73,128,73,133,73,134,73,135,73,140,73,145,73,
38 | 150,73,153,73,160,73,163,73,164,73,171,73,172,73,
39 | 176,73,180,73,187,73,192,73,201,73,203,73,205,73,
40 | 206,73,208,73,209,73,210,73,215,73,216,73,217,73,
41 | 221,73,222,73,223,73,224,73,227,73,231,73,238,73,
42 | 243,73,246,73,249,73,250,73,252,73,253,73,255,105,7,
43 | 105,8,105,10,105,19,105,41,105,21,105,22,105,23,105,
44 | 26,105,28,105,30,105,29,105,31,105,36,105,37,105,33,
45 | 105,34,105,46,105,45,105,11,105,50,105,51,105,53,
46 | 105,56,105,54,105,58,105,60,105,59,105,64,105,65,
47 | 105,67,105,68,105,75,105,82,105,85,105,86,105,90,
48 | 105,93,105,96,105,102,105,115,105,119,105,128,105,
49 | 129,105,130,105,131,105,132,105,136,105,138,105,144,
50 | 105,145,105,149,105,150,105,156,105,160,105,161,105,
51 | 166,105,173,105,178,105,179,105,180,105,181,105,187,
52 | 105,195,105,192,105,210,105,208,105,200,105,198,105,
53 | 205,105,203,105,216,105,213,105,223,105,224,105,225,
54 | 105,226,105,227,105,228,105,229,105,230,105,235,105,
55 | 237,105,244,105,250,105,245,105,255,137,3,137,4,137,
56 | 5,137,7,137,6,137,9,137,10,137,12,137,13,137,15,137,
57 | 26,137,29,137,49,137,53,137,54,137,56,137,63,137,69,
58 | 137,70,137,76,137,82,137,83,137,85,137,89,137,91,
59 | 137,95,137,96,137,97,137,101,137,102,137,106,137,
60 | 107,137,113,137,119,137,124,137,125,137,131,137,137,
61 | 137,138,137,147,137,148,137,150,137,152,137,154,137,
62 | 155,137,166,137,175,137,173,137,167,137,169,137,171,
63 | 137,181,137,186,137,187,137,194,137,202,137,203,137,
64 | 210,137,218,137,219,137,226,137,233,137,234,137,242,
65 | 137,243,137,245,137,250,137,252,169,2,169,3,169,5,
66 | 169,10,169,12,169,19,169,22,169,23,169,27,169,28,
67 | 169,30,169,31,169,32,169,33,169,39,169,40,169,44,
68 | 169,45,169,47,169,48,169,49,169,50,169,56,169,57,
69 | 169,61,169,62,169,64,169,65,169,66,169,71,169,73,
70 | 169,74,169,77,169,78,169,82,169,84,169,92,169,95,
71 | 169,99,169,106,169,100,169,111,169,107,169,114,169,
72 | 117,169,123,169,127,169,131,169,133,169,132,169,141,
73 | 169,142,169,143,169,147,169,148,169,153,169,154,169,
74 | 159,169,160,169,165,169,166,169,171,169,173,169,181,
75 | 169,182,169,189,169,190,169,198,169,199,169,210,169,
76 | 211,169,223,169,224,169,230,169,231,169,237,169,238,
77 | 169,244,169,245,169,248,169,251,169,252,201,4,201,5,
78 | 201,13,201,14,201,15,201,16,201,18,201,19,201,20,
79 | 201,21,201,23,201,24,201,28,201,44,201,45,201,47,
80 | 201,51,201,53,201,60,201,65,201,66,201,68,201,72,
81 | 201,74,201,83,201,84,201,87,201,88,201,89,201,93,
82 | 201,90,201,96,201,103,201,105,201,110,201,111,201,
83 | 132,201,134,201,136,201,139,201,141,201,162,201,163,
84 | 201,150,201,151,201,172,201,173,201,177,201,183,201,
85 | 184,201,186,201,187,201,189,201,190,201,194,201,195,
86 | 201,196,201,201,201,202,201,203,201,205,201,207,201,
87 | 206,201,209,201,210,201,211,201,213,201,214,201,216,
88 | 201,217,201,218,201,220,201,225,201,233,201,236,201,
89 | 234,201,238,201,244,201,239,201,247,201,249,201,250,
90 | 201,252,201,253,201,255,233,0,233,8,233,11,233,13,
91 | 233,17,233,18,233,25,233,26,233,27,233,29,233,30,
92 | 233,31,233,42,233,48,233,49,233,51,233,50,233,54,
93 | 233,55,233,56,233,59,233,60,233,64,233,61,233,66,
94 | 233,70,233,74,233,75,233,80,233,85,233,86,233,93,
95 | 233,94,233,98,233,99,233,100,233,106,233,107,233,
96 | 108,233,110,233,128,233,129,233,132,233,133,233,135,
97 | 233,136,233,139,233,140,233,143,233,144,233,150,233,
98 | 157,233,158,233,166,233,171,233,167,233,174,233,175,
99 | 233,179,233,180,233,185,233,198,233,199,233,200,233,
100 | 205,233,206,233,207,233,216,233,217,233,224,233,225,
101 | 233,235,233,239,233,242,233,253,25,8,3,25,8,4,25,8,
102 | 9,25,8,10,25,8,12,25,8,13,25,8,15,25,8,16,25,8,20,
103 | 25,8,23,25,8,26,25,8,27,25,8,35,25,8,36,25,8,39,25,
104 | 8,41,25,8,44,25,8,48,25,8,50,25,8,51,25,8,52,25,8,
105 | 53,25,8,54,25,8,57,25,8,62,25,8,65,25,8,66,25,8,74,
106 | 25,8,75,25,8,95,25,8,96,25,8,111,25,8,113,25,8,116,
107 | 25,8,118,25,8,125,25,8,129,25,8,128,25,8,143,25,8,
108 | 147,25,8,150,25,8,162,25,8,163,25,8,166,25,8,167,25,
109 | 8,154,25,8,172,25,8,174,25,8,175,25,8,176,25,8,177,
110 | 25,8,178,25,8,191,25,8,192,25,8,196,25,8,197,25,8,
111 | 201,25,8,202,25,8,206,25,8,207,25,8,212,25,8,213,25,
112 | 8,214,25,8,216,25,8,221,25,8,222,25,8,223,25,8,225,
113 | 25,8,229,25,8,230,25,8,235,25,8,240,25,8,241,25,8,
114 | 243,25,8,246,25,8,247,25,8,250,25,8,253,25,8,254,25,
115 | 9,2,25,9,3,25,9,11,25,9,12,25,9,13,25,9,14,25,9,28,
116 | 25,9,29,25,9,33,25,9,35,25,9,44,25,9,45,25,9,46,25,
117 | 9,48,25,9,50,25,9,51,25,9,52,25,9,55,25,9,56,25,9,
118 | 73,25,9,74,25,9,75,25,9,77,25,9,78,25,9,79,25,9,89,
119 | 25,9,90,25,9,94,25,9,95,25,9,105,25,9,106,25,9,107,
120 | 25,9,109,25,9,124,25,9,125,25,9,138,25,9,139,25,9,
121 | 149,25,9,154,25,9,157,25,9,170,25,9,174,25,9,175,25,
122 | 9,178,25,9,179,25,9,186,25,9,188,25,9,191,25,9,194,
123 | 25,9,197,25,9,202,25,9,207,25,9,208,25,9,209,25,9,
124 | 211,25,9,214,25,9,215,25,9,216,25,9,218,25,9,221,25,
125 | 9,222,25,9,223,25,9,225,25,9,228,25,9,229,25,9,230,
126 | 25,9,232,25,9,235,25,9,236,25,9,237,25,9,239,25,9,
127 | 242,25,9,243,25,9,244,25,9,246,25,9,140,25,9,141,25,
128 | 9,126,25,9,130,25,9,128,25,9,127,25,9,110,25,9,111,
129 | 25,9,112,25,9,113,25,9,115,25,9,96,25,9,97,25,9,98,
130 | 25,9,99,25,9,100,25,9,101,25,9,65,25,9,66,25,9,67,
131 | 25,9,68,25,9,69,25,9,57,25,9,58,25,9,62,25,9,59,25,
132 | 9,60,25,9,61,25,9,4,25,9,6,25,9,5,25,9,7,25,8,130,
133 | 25,8,132,25,8,131,25,8,134,25,8,135,25,8,133,25,8,
134 | 97,25,8,98,25,8,99,25,8,76,25,8,77,25,8,78,25,8,80,
135 | 25,8,82,25,8,83,25,8,85,25,8,86,25,8,87,25,8,88,25,
136 | 8,70,25,8,67,25,8,31,25,8,28,233,186,233,187,233,
137 | 109,233,68,233,67,233,63,233,52,233,12,233,1,201,
138 | 248,201,240,201,235,201,120,201,122,201,112,201,114,
139 | 201,117,201,115,201,91,201,92,201,26,201,25,201,6,
140 | 201,9,201,7,201,0,201,1,169,253,169,254,169,232,169,
141 | 233,169,234,169,235,169,225,169,226,169,227,169,220,
142 | 169,213,169,215,169,217,169,200,169,201,169,203,169,
143 | 204,169,205,169,206,169,191,169,192,169,193,169,194,
144 | 169,183,169,184,169,185,169,186,169,174,169,175,169,
145 | 176,169,177,169,167,169,168,169,161,169,162,169,155,
146 | 169,156,169,149,169,150,169,134,169,136,169,135,169,
147 | 129,169,128,169,108,169,103,169,79,169,80,169,75,
148 | 169,59,169,58,169,42,169,41,169,25,169,24,137,51,
149 | 137,50,137,47,137,44,137,43,137,42,137,41,137,40,
150 | 137,39,137,38,137,34,137,33,137,32,137,31,137,30,
151 | 137,16,137,18,137,23,137,20,137,1,105,167,105,104,
152 | 105,103,105,117,105,116,105,69,105,70,105,71,105,72,
153 | 105,61,105,55,105,12,105,15,105,16,73,211,73,212,73,
154 | 213,73,214,73,174,73,173,73,123,73,124,73,118,73,
155 | 106,73,105,73,50,73,51,73,53,73,54,73,55,73,30,73,
156 | 31,73,26,73,25,41,254,41,255,73,0,41,250,41,249,41,
157 | 197,41,198,41,192,41,193,41,183,41,182,41,160,41,
158 | 161,41,163,41,165,41,171,41,167,41,168,41,137,41,
159 | 138,41,148,41,140,41,145,41,141,41,142,41,143,41,
160 | 144,41,105,41,106,41,96,41,97,41,98,41,82,41,79,41,
161 | 90,41,87,41,1,9,248,9,249,9,250,9,242,9,239,9,231,9,
162 | 218,9,220,9,219,9,221,9,222,9,223,9,212,9,209,9,201,
163 | 9,203,9,204,9,202,9,192,9,194,9,195,9,193,9,183,9,
164 | 185,9,178,9,180,9,173,9,170,9,149,9,150>>,
165 | ?assertMatch(
166 | {lines,
167 | 1011,
168 | [{line, 0, _} | _],
169 | 1,
170 | ["rabbit_amqqueue.erl"],
171 | _},
172 | horus:decode_line_chunk(Module, Chunk)).
173 |
--------------------------------------------------------------------------------
/test/local_vs_external.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(local_vs_external).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | local_test() ->
16 | Fun = fun() -> ok end,
17 | StandaloneFun = horus:to_standalone_fun(Fun),
18 | ?assertStandaloneFun(StandaloneFun),
19 | ?assertMatch(ok, horus:exec(StandaloneFun, [])).
20 |
21 | external_test() ->
22 | Fun = fun erlang:abs/1,
23 | StandaloneFun = horus:to_standalone_fun(Fun),
24 | ?assert(is_function(StandaloneFun, 1)),
25 | ?assertEqual(Fun, StandaloneFun),
26 | ?assertMatch(1, horus:exec(StandaloneFun, [1])).
27 |
--------------------------------------------------------------------------------
/test/macros.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(macros).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("include/horus.hrl").
14 | -include("test/helpers.hrl").
15 |
16 | is_horus_fun_test() ->
17 | Fun = fun() -> ok end,
18 | ?assert(?IS_HORUS_FUN(Fun)),
19 |
20 | StandaloneFun = horus:to_standalone_fun(Fun),
21 | ?assertStandaloneFun(StandaloneFun),
22 | ?assert(?IS_HORUS_FUN(StandaloneFun)),
23 |
24 | ?assertNot(?IS_HORUS_FUN(not_a_horus_fun)).
25 |
26 | is_standalone_horus_fun_test() ->
27 | Fun = fun() -> ok end,
28 | ?assertNot(?IS_HORUS_STANDALONE_FUN(Fun)),
29 |
30 | StandaloneFun = horus:to_standalone_fun(Fun),
31 | ?assertStandaloneFun(StandaloneFun),
32 | ?assert(?IS_HORUS_STANDALONE_FUN(StandaloneFun)),
33 |
34 | ?assertNot(?IS_HORUS_STANDALONE_FUN(not_a_horus_fun)).
35 |
36 | is_standalone_horus_fun_arity_test() ->
37 | Fun = fun() -> ok end,
38 | ?assertNot(?IS_HORUS_STANDALONE_FUN(Fun, 0)),
39 |
40 | StandaloneFun = horus:to_standalone_fun(Fun),
41 | ?assertStandaloneFun(StandaloneFun),
42 | ?assert(?IS_HORUS_STANDALONE_FUN(StandaloneFun, 0)),
43 | ?assertNot(?IS_HORUS_STANDALONE_FUN(StandaloneFun, 1)),
44 |
45 | ?assertNot(?IS_HORUS_STANDALONE_FUN(not_a_horus_fun, 0)).
46 |
47 | standalone_horus_arity_test() ->
48 | Fun = fun() -> ok end,
49 | ?assertError(
50 | badarg,
51 | ?HORUS_STANDALONE_FUN_ARITY(helpers:ensure_not_optimized(Fun))),
52 |
53 | StandaloneFun = horus:to_standalone_fun(Fun),
54 | ?assertStandaloneFun(StandaloneFun),
55 | ?assertEqual(0, ?HORUS_STANDALONE_FUN_ARITY(StandaloneFun)),
56 |
57 | ?assertError(
58 | badarg,
59 | ?HORUS_STANDALONE_FUN_ARITY(
60 | helpers:ensure_not_optimized(not_a_horus_fun))).
61 |
--------------------------------------------------------------------------------
/test/misuses.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(misuses).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("include/horus.hrl").
14 | -include("src/horus_fun.hrl").
15 |
16 | -dialyzer([{no_missing_calls,
17 | [unknown_module_1_test/0,
18 | unknown_module_2_test/0,
19 | unknown_function_1_test/0,
20 | unknown_function_2_test/0,
21 | unexported_function_1_test/0,
22 | unexported_function_2_test/0]},
23 | {no_fail_call,
24 | [exec_wrong_arity_test/0]},
25 | {no_return,
26 | [exec_invalid_generated_module_test/0]}]).
27 |
28 | unknown_module_1_test() ->
29 | Fun = fun not_a_module:not_a_function/0,
30 | ?assertError(undef, Fun()),
31 | ?assertThrow(
32 | ?horus_error(
33 | call_to_unexported_function,
34 | #{mfa := {not_a_module, not_a_function, 0}}),
35 | horus:to_standalone_fun(Fun)).
36 |
37 | unknown_module_2_test() ->
38 | Fun = fun() -> not_a_module:not_a_function() end,
39 | ?assertError(undef, Fun()),
40 | ?assertThrow(
41 | ?horus_error(
42 | call_to_unexported_function,
43 | #{mfa := {not_a_module, not_a_function, 0}}),
44 | horus:to_standalone_fun(Fun)).
45 |
46 | unknown_function_1_test() ->
47 | Fun = fun compile:not_a_function/0,
48 | ?assertError(undef, Fun()),
49 | ?assertThrow(
50 | ?horus_error(
51 | call_to_unexported_function,
52 | #{mfa := {compile, not_a_function, 0}}),
53 | horus:to_standalone_fun(Fun)).
54 |
55 | unknown_function_2_test() ->
56 | Fun = fun() -> compile:not_a_function() end,
57 | ?assertError(undef, Fun()),
58 | ?assertThrow(
59 | ?horus_error(
60 | call_to_unexported_function,
61 | #{mfa := {compile, not_a_function, 0}}),
62 | horus:to_standalone_fun(Fun)).
63 |
64 | unexported_function_1_test() ->
65 | Fun = fun compile:env_default_opts/0,
66 | ?assertError(undef, Fun()),
67 | ?assertThrow(
68 | ?horus_error(
69 | call_to_unexported_function,
70 | #{mfa := {compile, env_default_opts, 0}}),
71 | horus:to_standalone_fun(Fun)).
72 |
73 | unexported_function_2_test() ->
74 | Fun = fun() -> compile:env_default_opts() end,
75 | ?assertError(undef, Fun()),
76 | ?assertThrow(
77 | ?horus_error(
78 | call_to_unexported_function,
79 | #{mfa := {compile, env_default_opts, 0}}),
80 | horus:to_standalone_fun(Fun)).
81 |
82 | exec_wrong_arity_test() ->
83 | Fun = fun(Arg) -> Arg end,
84 | StandaloneFun = horus:to_standalone_fun(Fun),
85 | ?assertError({badarity, {Fun, []}}, Fun()),
86 | ?assertError(
87 | {badarity, {StandaloneFun, []}},
88 | horus:exec(StandaloneFun, [])).
89 |
90 | exec_invalid_generated_module_test() ->
91 | StandaloneFun = #horus_fun{
92 | module = module_not_loaded,
93 | beam = <<"invalid">>,
94 | arity = 0,
95 | literal_funs = [],
96 | fun_name_mapping = #{},
97 | env = []},
98 | ?assertError(
99 | ?horus_exception(
100 | invalid_generated_module,
101 | #{horus_fun := StandaloneFun,
102 | error := {error, badfile}}),
103 | horus:exec(StandaloneFun, [])).
104 |
--------------------------------------------------------------------------------
/test/module_info.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(module_info).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | with_module_info_test() ->
16 | Fun = fun() -> ok end,
17 | StandaloneFun = horus:to_standalone_fun(
18 | Fun, #{add_module_info => true}),
19 | ?assertStandaloneFun(StandaloneFun),
20 | ?assertMatch(ok, horus:exec(StandaloneFun, [])),
21 |
22 | #horus_fun{module = Module} = StandaloneFun,
23 | ?assert(erlang:function_exported(Module, module_info, 0)),
24 | ?assert(erlang:function_exported(Module, module_info, 1)),
25 | ?assertEqual(erlang:get_module_info(Module), Module:module_info()).
26 |
27 | without_module_info_test() ->
28 | Fun = fun() -> ok end,
29 | StandaloneFun = horus:to_standalone_fun(
30 | Fun, #{add_module_info => false}),
31 | ?assertStandaloneFun(StandaloneFun),
32 | ?assertMatch(ok, horus:exec(StandaloneFun, [])),
33 |
34 | #horus_fun{module = Module} = StandaloneFun,
35 | ?assertNot(erlang:function_exported(Module, module_info, 0)),
36 | ?assertNot(erlang:function_exported(Module, module_info, 1)).
37 |
--------------------------------------------------------------------------------
/test/nested_funs.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(nested_funs).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("include/horus.hrl").
14 |
15 | -include("test/helpers.hrl").
16 |
17 | -dialyzer([{no_return,
18 | [call_fun2_instruction_with_atom_unsafe_test/0,
19 | call_outer_function_external/3]},
20 | {no_fail_call,
21 | [call_outer_function_external/3]}]).
22 |
23 | fun0_in_fun_env_test() ->
24 | Fun = make_fun(0),
25 | StandaloneFun = ?make_standalone_fun(Fun()),
26 | ?assertStandaloneFun(StandaloneFun),
27 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
28 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
29 |
30 | fun1_in_fun_env_test() ->
31 | Fun = make_fun(1),
32 | StandaloneFun = ?make_standalone_fun(Fun(1)),
33 | ?assertStandaloneFun(StandaloneFun),
34 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
35 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
36 |
37 | fun2_in_fun_env_test() ->
38 | Fun = make_fun(2),
39 | StandaloneFun = ?make_standalone_fun(Fun(1, 2)),
40 | ?assertStandaloneFun(StandaloneFun),
41 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
42 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
43 |
44 | fun3_in_fun_env_test() ->
45 | Fun = make_fun(3),
46 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3)),
47 | ?assertStandaloneFun(StandaloneFun),
48 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
49 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
50 |
51 | fun4_in_fun_env_test() ->
52 | Fun = make_fun(4),
53 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4)),
54 | ?assertStandaloneFun(StandaloneFun),
55 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
56 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
57 |
58 | fun5_in_fun_env_test() ->
59 | Fun = make_fun(5),
60 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5)),
61 | ?assertStandaloneFun(StandaloneFun),
62 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
63 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
64 |
65 | fun6_in_fun_env_test() ->
66 | Fun = make_fun(6),
67 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6)),
68 | ?assertStandaloneFun(StandaloneFun),
69 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
70 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
71 |
72 | fun7_in_fun_env_test() ->
73 | Fun = make_fun(7),
74 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7)),
75 | ?assertStandaloneFun(StandaloneFun),
76 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
77 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
78 |
79 | fun8_in_fun_env_test() ->
80 | Fun = make_fun(8),
81 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7, 8)),
82 | ?assertStandaloneFun(StandaloneFun),
83 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
84 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
85 |
86 | fun9_in_fun_env_test() ->
87 | Fun = make_fun(9),
88 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7, 8, 9)),
89 | ?assertStandaloneFun(StandaloneFun),
90 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
91 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
92 |
93 | fun10_in_fun_env_test() ->
94 | Fun = make_fun(10),
95 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)),
96 | ?assertStandaloneFun(StandaloneFun),
97 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
98 | ?assertEqual(result, horus:exec(StandaloneFun, [])).
99 |
100 | fun11_in_fun_env_test() ->
101 | Fun = make_fun(11),
102 | %% TODO: Raise an error at extraction time, not at execution time.
103 | StandaloneFun = ?make_standalone_fun(
104 | Fun(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)),
105 | ?assertStandaloneFun(StandaloneFun),
106 | ?assertNotEqual([], StandaloneFun#horus_fun.env),
107 | ?assertError(
108 | ?horus_exception(
109 | nested_fun_with_arity_too_great,
110 | #{arity := 11}),
111 | horus:exec(StandaloneFun, [])).
112 |
113 | make_fun(0) -> fun() -> result end;
114 | make_fun(1) -> fun(_) -> result end;
115 | make_fun(2) -> fun(_, _) -> result end;
116 | make_fun(3) -> fun(_, _, _) -> result end;
117 | make_fun(4) -> fun(_, _, _, _) -> result end;
118 | make_fun(5) -> fun(_, _, _, _, _) -> result end;
119 | make_fun(6) -> fun(_, _, _, _, _, _) -> result end;
120 | make_fun(7) -> fun(_, _, _, _, _, _, _) -> result end;
121 | make_fun(8) -> fun(_, _, _, _, _, _, _, _) -> result end;
122 | make_fun(9) -> fun(_, _, _, _, _, _, _, _, _) -> result end;
123 | make_fun(10) -> fun(_, _, _, _, _, _, _, _, _, _) -> result end;
124 | make_fun(11) -> fun(_, _, _, _, _, _, _, _, _, _, _) -> result end.
125 |
126 | higher_order_external_call_test() ->
127 | StandaloneFun = ?make_standalone_fun(
128 | begin
129 | Fun = fun arbitrary_mod:min/2,
130 | apply_fun_to_args(Fun, 1, 2)
131 | end),
132 | ?assertStandaloneFun(StandaloneFun),
133 | ?assertEqual(1, horus:exec(StandaloneFun, [])).
134 |
135 | apply_fun_to_args(Fun, Arg1, Arg2) ->
136 | Fun(Arg1, Arg2).
137 |
138 | multiple_nested_higher_order_functions_test() ->
139 | StandaloneFun = ?make_standalone_fun(
140 | begin
141 | MapFun = fun(X, Y) -> {X, Y} end,
142 | Fun = fun projection_fun_for_sets/1,
143 | Fun(MapFun)
144 | end),
145 | ?assertStandaloneFun(StandaloneFun),
146 | Ret1 = horus:exec(StandaloneFun, []),
147 | ?assert(is_function(Ret1, 3)),
148 |
149 | Sets = sets:from_list([a, b]),
150 | Ret2 = Ret1(Sets, path, change),
151 | ?assertEqual([{change, {path, a}}, {change, {path, b}}], Ret2).
152 |
153 | projection_fun_for_sets(MapFun) ->
154 | ChangesFromSet =
155 | fun(Set, Path, Change) ->
156 | sets:fold(fun(Element, Acc) ->
157 | [{Change, MapFun(Path, Element)} | Acc]
158 | end, [], Set)
159 | end,
160 | fun(Set, Path, Change) ->
161 | ChangesFromSet(Set, Path, Change)
162 | end.
163 |
164 | call_fun2_instruction_with_atom_unsafe_test() ->
165 | OuterFun = fun(Ret) -> {outer, Ret} end,
166 | InnerFun = fun inner_function/1,
167 |
168 | StandaloneFun = ?make_standalone_fun(
169 | begin
170 | R = call_outer_function_external(
171 | OuterFun, InnerFun, #{}),
172 | {ok, R}
173 | end),
174 | ?assertStandaloneFun(StandaloneFun),
175 | ?assertError({badarity, {_, [#{}]}}, horus:exec(StandaloneFun, [])).
176 |
177 | -spec call_outer_function_external(fun(), fun(), map()) -> no_return().
178 |
179 | call_outer_function_external(OuterFun, InnerFun, Options) ->
180 | OuterFun(
181 | arbitrary_mod:call_inner_function(
182 | fun() -> InnerFun() end,
183 | Options)).
184 |
185 | call_fun2_instruction_with_jump_label_test() ->
186 | OuterFun = fun(Ret) -> {outer, Ret} end,
187 | InnerInnerFun = fun inner_function/1,
188 | InnerFun = fun() ->
189 | case erlang:phash2(a) of
190 | H when H > 1 -> InnerInnerFun(H);
191 | _ -> error
192 | end
193 | end,
194 |
195 | StandaloneFun = ?make_standalone_fun(
196 | begin
197 | R = call_outer_function_local(
198 | OuterFun, InnerFun, #{}),
199 | {ok, R}
200 | end),
201 | ?assertStandaloneFun(StandaloneFun),
202 | ?assertEqual({ok, {outer, inner}}, horus:exec(StandaloneFun, [])).
203 |
204 | call_outer_function_local(OuterFun, InnerFun, Options) ->
205 | OuterFun(
206 | call_inner_function(
207 | fun() -> InnerFun() end,
208 | Options)).
209 |
210 | call_inner_function(InnerFun, Options) when is_map(Options) ->
211 | InnerFun().
212 |
213 | inner_function(_) ->
214 | inner.
215 |
--------------------------------------------------------------------------------
/test/to_fun.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(to_fun).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | standalone_fun_to_fun_test() ->
16 | Fun = fun() -> ok end,
17 | StandaloneFun = horus:to_standalone_fun(Fun),
18 | ?assertStandaloneFun(StandaloneFun),
19 | NewFun = horus:to_fun(StandaloneFun),
20 | ?assert(is_function(NewFun, 0)),
21 | ?assertEqual(Fun(), NewFun()).
22 |
23 | fun_to_fun_test() ->
24 | Fun = fun erlang:abs/1,
25 | StandaloneFun = horus:to_standalone_fun(Fun),
26 | ?assert(is_function(StandaloneFun, 1)),
27 | NewFun = horus:to_fun(StandaloneFun),
28 | ?assert(is_function(NewFun, 1)),
29 | ?assertEqual(Fun(-2), NewFun(-2)).
30 |
--------------------------------------------------------------------------------
/test/using_erl_eval.erl:
--------------------------------------------------------------------------------
1 | %% This Source Code Form is subject to the terms of the Mozilla Public
2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this
3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 | %%
5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom"
6 | %% refers to Broadcom Inc. and/or its subsidiaries.
7 | %%
8 |
9 | -module(using_erl_eval).
10 |
11 | -include_lib("eunit/include/eunit.hrl").
12 |
13 | -include("test/helpers.hrl").
14 |
15 | -define(TX_CODE, "Fun = fun(Arg) -> erlang:abs(Arg) end,
16 | horus:to_standalone_fun(Fun).").
17 |
18 | erl_eval_test() ->
19 | Bindings = erl_eval:new_bindings(),
20 | {ok, Tokens, _EndLocation} = erl_scan:string(?TX_CODE),
21 | {ok, Exprs} = erl_parse:parse_exprs(Tokens),
22 | {value, StandaloneFun, _NewBindings} = erl_eval:exprs(Exprs, Bindings),
23 | ?assertStandaloneFun(StandaloneFun),
24 | ?assertEqual(2, horus:exec(StandaloneFun, [-2])).
25 |
--------------------------------------------------------------------------------