Compare commits

...

19 commits

Author SHA1 Message Date
Ajay
f90b66758b
Merge 918231a1ad into 9800355131 2025-03-28 20:34:12 -04:00
Ukendio
9800355131 Styling on coverage reports
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-03-28 02:29:55 +01:00
Ukendio
dd9db448e3 New coverage reports 2025-03-28 01:53:19 +01:00
Ukendio
34777b51c4 Fix typos 2025-03-28 01:52:33 +01:00
Ukendio
4eb15bbb55 Update removal functions to handle more variants 2025-03-27 21:49:44 +01:00
Ukendio
b81bd6eea8 Make tests lazily evaluated 2025-03-27 17:37:36 +01:00
Ukendio
e6039caaaf Revert mirror back to pinned version
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-27 06:33:41 +01:00
Ukendio
5aec94cb5f Remove file
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-27 05:08:55 +01:00
Ukendio
75f2584bf4 Clear ID will remove ID from all entities 2025-03-27 05:08:38 +01:00
Ukendio
4d6c2ccbb8 Change generic to unknown 2025-03-27 04:17:23 +01:00
Ukendio
bf33748ecf Exclude html coverage reports from language stats 2025-03-27 04:09:09 +01:00
Ukendio
324f0fa9a2 Add html files to coverage folder 2025-03-27 03:55:43 +01:00
Ukendio
1561e446b8 Add html files to coverage folder 2025-03-27 03:55:16 +01:00
Ukendio
9a22b38bb3 Reorganize repo 2025-03-27 02:33:38 +01:00
unknown
58e67eda0d Target should return the entity with its generation
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-26 14:23:54 +01:00
Ukendio
045408af37 Rollback types
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-26 04:59:11 +01:00
Ukendio
59e7fd1f41 Fix nth count for target 2025-03-26 03:39:04 +01:00
Ukendio
f3befa3adb Add dual types 2025-03-25 23:13:53 +01:00
aybanda
918231a1ad Start work on #50 2025-02-19 16:36:07 +05:30
47 changed files with 13080 additions and 3972 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
*.luau text eol=lf
*.html linguist-vendored

View file

@ -1,22 +1,25 @@
--- ---
name: Bug report name: Bug report
about: File a bug report for any behavior that you believe is unintentional or problematic about: File a bug report for any behavior that you believe is unintentional or problematic
title: "[BUG]" title: ""
labels: bug labels: bug
assignees: '' assignees: ""
---
---
## Describe the bug
## Describe the bug
Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps. Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps.
## Reproduction ## Reproduction
Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue.
Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue.
This is a good place to put rbxl files or scripts that help explain your reproduction steps.
This is a good place to put rbxl files or scripts that help explain your reproduction steps.
## Expected Behavior
What you expect to happen ## Expected Behavior
## Actual Behavior What you expect to happen
What actually happens
## Actual Behavior
What actually happens

View file

@ -1,14 +1,15 @@
--- ---
name: Documentation name: Documentation
about: Open an issue to add, change, or otherwise modify any part of the documentation. about: Open an issue to add, change, or otherwise modify any part of the documentation.
title: "[DOCS]" title: ""
labels: documentation labels: documentation
assignees: '' assignees: ""
--- ---
## Which Sections Does This Issue Cover? ## Which Sections Does This Issue Cover?
[Put sections (e.g. Query Concepts), page links, etc as necessary] [Put sections (e.g. Query Concepts), page links, etc as necessary]
## What Needs To Change? ## What Needs To Change?
What specifically needs to change and what suggestions do you have to change it?
What specifically needs to change and what suggestions do you have to change it?

View file

@ -1,27 +1,27 @@
--- ---
name: Feature Request name: Feature Request
about: File a feature request for something you believe should be added to Jecs about: File a feature request for something you believe should be added to Jecs
title: "[FEATURE]" title: ""
labels: enhancement labels: enhancement
assignees: '' assignees: ""
---
---
## Describe your Feature
## Describe your Feature
You should explain your feature here, and the motivation for why you want it.
You should explain your feature here, and the motivation for why you want it.
## Implementation
## Implementation
Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable).
Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable).
## Alternatives
## Alternatives
What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation?
What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation?
## Considerations
## Considerations
Some questions that need to be answered include the following:
Some questions that need to be answered include the following:
- Will old code break in response to this feature? - Will old code break in response to this feature?
- What are the performance impacts with this feature (if any)? - What are the performance impacts with this feature (if any)?
- How is it useful to include? - How is it useful to include?

2
.gitignore vendored
View file

@ -69,3 +69,5 @@ profile.*
# Patch files # Patch files
*.patch *.patch
genhtml.perl

View file

@ -1,11 +1,9 @@
column_width = 120 syntax = "All"
line_endings = "Unix" column_width = 120
indent_type = "Tabs" line_endings = "Unix"
indent_width = 4 indent_type = "Tabs"
quote_style = "ForceDouble" indent_width = 4
call_parentheses = "Always" quote_style = "AutoPreferDouble"
collapse_simple_statement = "Never" call_parentheses = "Always"
syntax = "Luau" space_after_function_names = "Never"
collapse_simple_statement = "Never"
[sort_requires]
enabled = true

View file

@ -0,0 +1,22 @@
---
name: Bug report
about: File a bug report for any behavior that you believe is unintentional or problematic
title: "[BUG]"
labels: bug
assignees: ''
---
## Describe the bug
Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps.
## Reproduction
Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue.
This is a good place to put rbxl files or scripts that help explain your reproduction steps.
## Expected Behavior
What you expect to happen
## Actual Behavior
What actually happens

View file

@ -0,0 +1,14 @@
---
name: Documentation
about: Open an issue to add, change, or otherwise modify any part of the documentation.
title: "[DOCS]"
labels: documentation
assignees: ''
---
## Which Sections Does This Issue Cover?
[Put sections (e.g. Query Concepts), page links, etc as necessary]
## What Needs To Change?
What specifically needs to change and what suggestions do you have to change it?

View file

@ -0,0 +1,27 @@
---
name: Feature Request
about: File a feature request for something you believe should be added to Jecs
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
## Describe your Feature
You should explain your feature here, and the motivation for why you want it.
## Implementation
Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable).
## Alternatives
What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation?
## Considerations
Some questions that need to be answered include the following:
- Will old code break in response to this feature?
- What are the performance impacts with this feature (if any)?
- How is it useful to include?

15
assets/.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,15 @@
## Brief Description of your Changes.
Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
## Impact of your Changes
What implications will this have on the project? Will there be altered behavior or performance with this change?
## Tests Performed
What have you done to ensure this change has the least possible impact on the project?
## Additional Comments
Anything else you feel is relevant.

19
assets/.github/workflows/analysis.yaml vendored Normal file
View file

@ -0,0 +1,19 @@
name: analysis
on: [push, pull_request, workflow_dispatch]
jobs:
run:
name: Run Luau Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Luau
uses: encodedvenom/install-luau@v2.1
- name: Analyze
run: |
output=$(luau-analyze src || true) # Suppress errors for now.

11
assets/.github/workflows/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: npm
directory: "/"
schedule:
interval: "daily"

View file

@ -0,0 +1,64 @@
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: deploy-docs
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -0,0 +1,17 @@
name: publish-npm
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: "20"
- uses: JS-DevTools/npm-publish@v3
with:
token: ${{ secrets.NPM_AUTH_TOKEN }}

71
assets/.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,71 @@
name: release
on:
push:
tags: ["v*"]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: Install Dependencies
run: wally install
- name: Build
run: rojo build --output build.rbxm default.project.json
- name: Upload Build Artifact
uses: actions/upload-artifact@v3
with:
name: build
path: build.rbxm
release:
name: Release
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Download Jecs Build
uses: actions/download-artifact@v3
with:
name: build
path: build
- name: Rename Build
run: mv build/build.rbxm jecs.rbxm
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: Jecs ${{ github.ref_name }}
files: |
jecs.rbxm
publish:
name: Publish
needs: [release]
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: Wally Login
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
- name: Publish
run: wally publish

View file

@ -0,0 +1,31 @@
name: unit-testing
on: [push, pull_request, workflow_dispatch]
jobs:
run:
name: Run Luau Tests
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Luau
uses: encodedvenom/install-luau@v4.3
with:
version: "latest"
verbose: "true"
- name: Run Unit Tests
id: run_tests
run: |
output=$(luau test/tests.luau)
echo "$output"
if [[ "$output" == *"0 fails"* ]]; then
echo "Unit Tests Passed"
else
echo "Error: One or More Unit Tests Failed."
exit 1
fi

View file

@ -1,5 +1,5 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local mirror = require("../mirror/init") local mirror = require("@mirror")
type i53 = number type i53 = number

View file

@ -1,49 +1,49 @@
--!optimize 2 --!optimize 2
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter) local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib) local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair local pair = jecs.pair
local ecs = jecs.World.new() local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror) local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new() local mcs = mirror.World.new()
local C1 = ecs:component() local C1 = ecs:component()
local C2 = ecs:entity() local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
local C3 = ecs:entity() local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity() local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component() local E1 = mcs:component()
local E2 = mcs:entity() local E2 = mcs:entity()
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity() local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity() local E4 = mcs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete)) mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
return { return {
ParameterGenerator = function() ParameterGenerator = function()
end, end,
Functions = { Functions = {
Mirror = function() Mirror = function()
local m = mcs:entity() local m = mcs:entity()
for i = 1, 100 do for i = 1, 100 do
mcs:add(m, E3) mcs:add(m, E3)
mcs:remove(m, E3) mcs:remove(m, E3)
end end
end, end,
Jecs = function() Jecs = function()
local j = ecs:entity() local j = ecs:entity()
for i = 1, 100 do for i = 1, 100 do
ecs:add(j, C3) ecs:add(j, C3)
ecs:remove(j, C3) ecs:remove(j, C3)
end end
end, end,
}, },
} }

69
coverage/ansi.luau.html vendored Normal file
View file

@ -0,0 +1,69 @@
<html><head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<style>
body { font-family: monospace; text-align: center; }
#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }
#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }
#funcTable th { background-color: #ddd; }
#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }
.zero-hits { background-color: #fcc; font-weight: bold; color: red; }
.nonzero-hits { color: green; font-weight: bold; }
.low-hits { background-color: #ffe6b3; }
.high-hits { background-color: #cfc; }
.source-code-table { margin-left: 10px; }th, td { padding: 0px; font-size: 12px; }
table.table { font-size: 14px; border-collapse: collapse; }
table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }
table.table tr { height: auto; }
</style></head><body>
<h1 class="text-center">ansi.luau Coverage</h1>
<h2>Total Execution Hits: 1</h2>
<h2>Function Coverage Overview: 11.11%</h2>
<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">Toggle Function Coverage</button>
<div class="collapse show" id="funcTable">
<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>
<tr><td style="padding: 1px; min-width: 18ch;"><main></td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">white_underline:2</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">white:6</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">green:10</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">red:14</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">yellow:18</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">red_highlight:22</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">green_highlight:26</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 18ch;">gray:30</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
</tbody></table></div>
<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>
<tr><td>1</td><td>1</td><td><span class=high-hits>return {</span></td></tr>
<tr><td>2</td><td>1</td><td><span class=high-hits>white_underline = function(s: any)</span></td></tr>
<tr><td>3</td><td>0</td><td><span class=zero-hits>return `\27[1;4m{s}\27[0m`</span></td></tr>
<tr><td>4</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>5</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>6</td><td>1</td><td><span class=high-hits>white = function(s: any)</span></td></tr>
<tr><td>7</td><td>0</td><td><span class=zero-hits>return `\27[37;1m{s}\27[0m`</span></td></tr>
<tr><td>8</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>9</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>10</td><td>1</td><td><span class=high-hits>green = function(s: any)</span></td></tr>
<tr><td>11</td><td>0</td><td><span class=zero-hits>return `\27[32;1m{s}\27[0m`</span></td></tr>
<tr><td>12</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>13</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>14</td><td>1</td><td><span class=high-hits>red = function(s: any)</span></td></tr>
<tr><td>15</td><td>0</td><td><span class=zero-hits>return `\27[31;1m{s}\27[0m`</span></td></tr>
<tr><td>16</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>17</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>18</td><td>1</td><td><span class=high-hits>yellow = function(s: any)</span></td></tr>
<tr><td>19</td><td>0</td><td><span class=zero-hits>return `\27[33;1m{s}\27[0m`</span></td></tr>
<tr><td>20</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>21</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>22</td><td>1</td><td><span class=high-hits>red_highlight = function(s: any)</span></td></tr>
<tr><td>23</td><td>0</td><td><span class=zero-hits>return `\27[41;1;30m{s}\27[0m`</span></td></tr>
<tr><td>24</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>25</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>26</td><td>1</td><td><span class=high-hits>green_highlight = function(s: any)</span></td></tr>
<tr><td>27</td><td>0</td><td><span class=zero-hits>return `\27[42;1;30m{s}\27[0m`</span></td></tr>
<tr><td>28</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>29</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>30</td><td>1</td><td><span class=high-hits>gray = function(s: any)</span></td></tr>
<tr><td>31</td><td>0</td><td><span class=zero-hits>return `\27[30;1m{s}\27[0m`</span></td></tr>
<tr><td>32</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>33</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
</tbody></table></body></html>

74
coverage/entity_visualiser.luau.html vendored Normal file
View file

@ -0,0 +1,74 @@
<html><head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<style>
body { font-family: monospace; text-align: center; }
#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }
#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }
#funcTable th { background-color: #ddd; }
#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }
.zero-hits { background-color: #fcc; font-weight: bold; color: red; }
.nonzero-hits { color: green; font-weight: bold; }
.low-hits { background-color: #ffe6b3; }
.high-hits { background-color: #cfc; }
.source-code-table { margin-left: 10px; }th, td { padding: 0px; font-size: 12px; }
table.table { font-size: 14px; border-collapse: collapse; }
table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }
table.table tr { height: auto; }
</style></head><body>
<h1 class="text-center">entity_visualiser.luau Coverage</h1>
<h2>Total Execution Hits: 1</h2>
<h2>Function Coverage Overview: 25.00%</h2>
<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">Toggle Function Coverage</button>
<div class="collapse show" id="funcTable">
<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>
<tr><td style="padding: 1px; min-width: 13ch;"><main></td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 13ch;">pe:6</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 13ch;">name:11</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 13ch;">components:15</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
</tbody></table></div>
<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>
<tr><td>1</td><td>1</td><td><span class=high-hits>local jecs = require("@jecs")</span></td></tr>
<tr><td>2</td><td>1</td><td><span class=high-hits>local ECS_GENERATION = jecs.ECS_GENERATION</span></td></tr>
<tr><td>3</td><td>1</td><td><span class=high-hits>local ECS_ID = jecs.ECS_ID</span></td></tr>
<tr><td>4</td><td>1</td><td><span class=high-hits>local ansi = require("@tools/ansi")</span></td></tr>
<tr><td>5</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>6</td><td>1</td><td><span class=high-hits>local function pe(e: any)</span></td></tr>
<tr><td>7</td><td>0</td><td><span class=zero-hits>local gen = ECS_GENERATION(e)</span></td></tr>
<tr><td>8</td><td>0</td><td><span class=zero-hits>return ansi.green(`e{ECS_ID(e)}`) .. ansi.yellow(`v{gen}`)</span></td></tr>
<tr><td>9</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>10</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>11</td><td>1</td><td><span class=high-hits>local function name(world: jecs.World, id: any)</span></td></tr>
<tr><td>12</td><td>0</td><td><span class=zero-hits>return world:get(id, jecs.Name) or `${id}`</span></td></tr>
<tr><td>13</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>14</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>15</td><td>1</td><td><span class=high-hits>local function components(world: jecs.World, entity: any)</span></td></tr>
<tr><td>16</td><td>0</td><td><span class=zero-hits>local r = jecs.entity_index_try_get(world.entity_index, entity)</span></td></tr>
<tr><td>17</td><td>0</td><td><span class=zero-hits>if not r then</span></td></tr>
<tr><td>18</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>19</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>20</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>21</td><td>0</td><td><span class=zero-hits>local archetype = r.archetype</span></td></tr>
<tr><td>22</td><td>0</td><td><span class=zero-hits>local row = r.row</span></td></tr>
<tr><td>23</td><td>0</td><td><span class=zero-hits>print(`Entity {pe(entity)}`)</span></td></tr>
<tr><td>24</td><td>0</td><td><span class=zero-hits>print("-----------------------------------------------------")</span></td></tr>
<tr><td>25</td><td>0</td><td><span class=zero-hits>for i, column in archetype.columns do</span></td></tr>
<tr><td>26</td><td>0</td><td><span class=zero-hits>local component = archetype.types[i]</span></td></tr>
<tr><td>27</td><td>0</td><td><span class=zero-hits>local n</span></td></tr>
<tr><td>28</td><td>0</td><td><span class=zero-hits>if jecs.IS_PAIR(component) then</span></td></tr>
<tr><td>29</td><td>0</td><td><span class=zero-hits>n = `({name(world, jecs.pair_first(world, component))}, {name(world, jecs.pair_second(world, component))})`</span></td></tr>
<tr><td>30</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>31</td><td>0</td><td><span class=zero-hits>n = name(world, component)</span></td></tr>
<tr><td>32</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>33</td><td>0</td><td><span class=zero-hits>local data = column[row] or "TAG"</span></td></tr>
<tr><td>34</td><td>0</td><td><span class=zero-hits>print(`| {n} | {data} |`)</span></td></tr>
<tr><td>35</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>36</td><td>0</td><td><span class=zero-hits>print("-----------------------------------------------------")</span></td></tr>
<tr><td>37</td><td>0</td><td><span class=zero-hits>return true</span></td></tr>
<tr><td>38</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>39</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>40</td><td>1</td><td><span class=high-hits>return {</span></td></tr>
<tr><td>41</td><td>1</td><td><span class=high-hits>components = components,</span></td></tr>
<tr><td>42</td><td>1</td><td><span class=high-hits>prettify = pe,</span></td></tr>
<tr><td>43</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
</tbody></table></body></html>

12
coverage/index.html vendored Normal file
View file

@ -0,0 +1,12 @@
<html><head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
</head><body>
<h1 class="text-center">Coverage Report</h1>
<table class="table table-striped table-bordered"><thead><tr><th>File</th><th>Total Hits</th><th>Functions</th></tr></thead><tbody>
<tr><td><a href="tests.luau.html">tests.luau</a></td><td>100</td><td>67</td></tr>
<tr><td><a href="jecs.luau.html">jecs.luau</a></td><td>1006447</td><td>97</td></tr>
<tr><td><a href="testkit.luau.html">testkit.luau</a></td><td>1826</td><td>31</td></tr>
<tr><td><a href="lifetime_tracker.luau.html">lifetime_tracker.luau</a></td><td>1</td><td>11</td></tr>
<tr><td><a href="entity_visualiser.luau.html">entity_visualiser.luau</a></td><td>1</td><td>4</td></tr>
<tr><td><a href="ansi.luau.html">ansi.luau</a></td><td>1</td><td>9</td></tr>
</tbody></table></body></html>

2798
coverage/jecs.luau.html vendored Normal file

File diff suppressed because it is too large Load diff

254
coverage/lifetime_tracker.luau.html vendored Normal file
View file

@ -0,0 +1,254 @@
<html><head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<style>
body { font-family: monospace; text-align: center; }
#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }
#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }
#funcTable th { background-color: #ddd; }
#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }
.zero-hits { background-color: #fcc; font-weight: bold; color: red; }
.nonzero-hits { color: green; font-weight: bold; }
.low-hits { background-color: #ffe6b3; }
.high-hits { background-color: #cfc; }
.source-code-table { margin-left: 10px; }th, td { padding: 0px; font-size: 12px; }
table.table { font-size: 14px; border-collapse: collapse; }
table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }
table.table tr { height: auto; }
</style></head><body>
<h1 class="text-center">lifetime_tracker.luau Coverage</h1>
<h2>Total Execution Hits: 1</h2>
<h2>Function Coverage Overview: 9.09%</h2>
<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">Toggle Function Coverage</button>
<div class="collapse show" id="funcTable">
<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>
<tr><td style="padding: 1px; min-width: 24ch;"><main></td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;">print_centered_entity:12</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;">name:26</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;">pad:30</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;">lifetime_tracker_add:36</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;"><anonymous>:48</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;"><anonymous>:62</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;"><anonymous>:89</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;"><anonymous>:135</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;"><anonymous>:164</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 24ch;"><anonymous>:175</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
</tbody></table></div>
<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>
<tr><td>1</td><td>1</td><td><span class=high-hits>local jecs = require("@jecs")</span></td></tr>
<tr><td>2</td><td>1</td><td><span class=high-hits>local ECS_GENERATION = jecs.ECS_GENERATION</span></td></tr>
<tr><td>3</td><td>1</td><td><span class=high-hits>local ECS_ID = jecs.ECS_ID</span></td></tr>
<tr><td>4</td><td>1</td><td><span class=high-hits>local __ = jecs.Wildcard</span></td></tr>
<tr><td>5</td><td>1</td><td><span class=high-hits>local pair = jecs.pair</span></td></tr>
<tr><td>6</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>7</td><td>1</td><td><span class=high-hits>local prettify = require("@tools/entity_visualiser").prettify</span></td></tr>
<tr><td>8</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>9</td><td>1</td><td><span class=high-hits>local pe = prettify</span></td></tr>
<tr><td>10</td><td>1</td><td><span class=high-hits>local ansi = require("@tools/ansi")</span></td></tr>
<tr><td>11</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>12</td><td>1</td><td><span class=high-hits>function print_centered_entity(entity, width: number)</span></td></tr>
<tr><td>13</td><td>0</td><td><span class=zero-hits>local entity_str = tostring(entity)</span></td></tr>
<tr><td>14</td><td>0</td><td><span class=zero-hits>local entity_length = #entity_str</span></td></tr>
<tr><td>15</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>16</td><td>0</td><td><span class=zero-hits>local padding_total = width - 2 - entity_length</span></td></tr>
<tr><td>17</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>18</td><td>0</td><td><span class=zero-hits>local padding_left = math.floor(padding_total / 2)</span></td></tr>
<tr><td>19</td><td>0</td><td><span class=zero-hits>local padding_right = padding_total - padding_left</span></td></tr>
<tr><td>20</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>21</td><td>0</td><td><span class=zero-hits>local centered_str = string.rep(" ", padding_left) .. entity_str .. string.rep(" ", padding_right)</span></td></tr>
<tr><td>22</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>23</td><td>0</td><td><span class=zero-hits>print("|" .. centered_str .. "|")</span></td></tr>
<tr><td>24</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>25</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>26</td><td>1</td><td><span class=high-hits>local function name(world, e)</span></td></tr>
<tr><td>27</td><td>0</td><td><span class=zero-hits>return world:get(world, e, jecs.Name) or pe(e)</span></td></tr>
<tr><td>28</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>29</td><td>1</td><td><span class=high-hits>local padding_enabled = false</span></td></tr>
<tr><td>30</td><td>1</td><td><span class=high-hits>local function pad()</span></td></tr>
<tr><td>31</td><td>0</td><td><span class=zero-hits>if padding_enabled then</span></td></tr>
<tr><td>32</td><td>0</td><td><span class=zero-hits>print("")</span></td></tr>
<tr><td>33</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>34</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>35</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>36</td><td>1</td><td><span class=high-hits>local function lifetime_tracker_add(world: jecs.World, opt)</span></td></tr>
<tr><td>37</td><td>0</td><td><span class=zero-hits>local entity_index = world.entity_index</span></td></tr>
<tr><td>38</td><td>0</td><td><span class=zero-hits>local dense_array = entity_index.dense_array</span></td></tr>
<tr><td>39</td><td>0</td><td><span class=zero-hits>local component_index = world.component_index</span></td></tr>
<tr><td>40</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>41</td><td>0</td><td><span class=zero-hits>local ENTITY_RANGE = (jecs.Rest :: any) + 1</span></td></tr>
<tr><td>42</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>43</td><td>0</td><td><span class=zero-hits>local w = setmetatable({}, { __index = world })</span></td></tr>
<tr><td>44</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>45</td><td>0</td><td><span class=zero-hits>padding_enabled = opt.padding_enabled</span></td></tr>
<tr><td>46</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>47</td><td>0</td><td><span class=zero-hits>local world_entity = world.entity</span></td></tr>
<tr><td>48</td><td>0</td><td><span class=zero-hits>w.entity = function(self, entity)</span></td></tr>
<tr><td>49</td><td>0</td><td><span class=zero-hits>if entity then</span></td></tr>
<tr><td>50</td><td>0</td><td><span class=zero-hits>return world_entity(world, entity)</span></td></tr>
<tr><td>51</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>52</td><td>0</td><td><span class=zero-hits>local will_recycle = entity_index.max_id ~= entity_index.alive_count</span></td></tr>
<tr><td>53</td><td>0</td><td><span class=zero-hits>local e = world_entity(world)</span></td></tr>
<tr><td>54</td><td>0</td><td><span class=zero-hits>if will_recycle then</span></td></tr>
<tr><td>55</td><td>0</td><td><span class=zero-hits>print(`*recycled {pe(e)}`)</span></td></tr>
<tr><td>56</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>57</td><td>0</td><td><span class=zero-hits>print(`*created {pe(e)}`)</span></td></tr>
<tr><td>58</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>59</td><td>0</td><td><span class=zero-hits>pad()</span></td></tr>
<tr><td>60</td><td>0</td><td><span class=zero-hits>return e</span></td></tr>
<tr><td>61</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>62</td><td>0</td><td><span class=zero-hits>w.print_entity_index = function(self)</span></td></tr>
<tr><td>63</td><td>0</td><td><span class=zero-hits>local max_id = entity_index.max_id</span></td></tr>
<tr><td>64</td><td>0</td><td><span class=zero-hits>local alive_count = entity_index.alive_count</span></td></tr>
<tr><td>65</td><td>0</td><td><span class=zero-hits>local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})</span></td></tr>
<tr><td>66</td><td>0</td><td><span class=zero-hits>local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})</span></td></tr>
<tr><td>67</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>68</td><td>0</td><td><span class=zero-hits>local sep = "|--------|"</span></td></tr>
<tr><td>69</td><td>0</td><td><span class=zero-hits>if #alive > 0 then</span></td></tr>
<tr><td>70</td><td>0</td><td><span class=zero-hits>print("|-alive--|")</span></td></tr>
<tr><td>71</td><td>0</td><td><span class=zero-hits>for i = 1, #alive do</span></td></tr>
<tr><td>72</td><td>0</td><td><span class=zero-hits>local e = pe(alive[i])</span></td></tr>
<tr><td>73</td><td>0</td><td><span class=zero-hits>print_centered_entity(e, 32)</span></td></tr>
<tr><td>74</td><td>0</td><td><span class=zero-hits>print(sep)</span></td></tr>
<tr><td>75</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>76</td><td>0</td><td><span class=zero-hits>print("\n")</span></td></tr>
<tr><td>77</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>78</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>79</td><td>0</td><td><span class=zero-hits>if #dead > 0 then</span></td></tr>
<tr><td>80</td><td>0</td><td><span class=zero-hits>print("|--dead--|")</span></td></tr>
<tr><td>81</td><td>0</td><td><span class=zero-hits>for i = 1, #dead do</span></td></tr>
<tr><td>82</td><td>0</td><td><span class=zero-hits>print_centered_entity(pe(dead[i]), 32)</span></td></tr>
<tr><td>83</td><td>0</td><td><span class=zero-hits>print(sep)</span></td></tr>
<tr><td>84</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>85</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>86</td><td>0</td><td><span class=zero-hits>pad()</span></td></tr>
<tr><td>87</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>88</td><td>0</td><td><span class=zero-hits>local timelines = {}</span></td></tr>
<tr><td>89</td><td>0</td><td><span class=zero-hits>w.print_snapshot = function(self)</span></td></tr>
<tr><td>90</td><td>0</td><td><span class=zero-hits>local timeline = #timelines + 1</span></td></tr>
<tr><td>91</td><td>0</td><td><span class=zero-hits>local entity_column_width = 10</span></td></tr>
<tr><td>92</td><td>0</td><td><span class=zero-hits>local status_column_width = 8</span></td></tr>
<tr><td>93</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>94</td><td>0</td><td><span class=zero-hits>local header = string.format("| %-" .. entity_column_width .. "s |", "Entity")</span></td></tr>
<tr><td>95</td><td>0</td><td><span class=zero-hits>for i = 1, timeline do</span></td></tr>
<tr><td>96</td><td>0</td><td><span class=zero-hits>header = header .. string.format(" %-" .. status_column_width .. "s |", string.format("T%d", i))</span></td></tr>
<tr><td>97</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>98</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>99</td><td>0</td><td><span class=zero-hits>local max_id = entity_index.max_id</span></td></tr>
<tr><td>100</td><td>0</td><td><span class=zero-hits>local alive_count = entity_index.alive_count</span></td></tr>
<tr><td>101</td><td>0</td><td><span class=zero-hits>local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})</span></td></tr>
<tr><td>102</td><td>0</td><td><span class=zero-hits>local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})</span></td></tr>
<tr><td>103</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>104</td><td>0</td><td><span class=zero-hits>local data = {}</span></td></tr>
<tr><td>105</td><td>0</td><td><span class=zero-hits>print("-------------------------------------------------------------------")</span></td></tr>
<tr><td>106</td><td>0</td><td><span class=zero-hits>print(header)</span></td></tr>
<tr><td>107</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>108</td><td><span class='text-muted'>N/A</span></td><td>-- Store the snapshot data for this timeline</td>></tr>
<tr><td>109</td><td>0</td><td><span class=zero-hits>for i = ENTITY_RANGE, max_id do</span></td></tr>
<tr><td>110</td><td>0</td><td><span class=zero-hits>if dense_array[i] then</span></td></tr>
<tr><td>111</td><td>0</td><td><span class=zero-hits>local entity = dense_array[i]</span></td></tr>
<tr><td>112</td><td>0</td><td><span class=zero-hits>local id = ECS_ID(entity)</span></td></tr>
<tr><td>113</td><td>0</td><td><span class=zero-hits>local status = "alive"</span></td></tr>
<tr><td>114</td><td>0</td><td><span class=zero-hits>if not world:contains(entity) then</span></td></tr>
<tr><td>115</td><td>0</td><td><span class=zero-hits>status = "dead"</span></td></tr>
<tr><td>116</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>117</td><td>0</td><td><span class=zero-hits>data[id] = status</span></td></tr>
<tr><td>118</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>119</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>120</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>121</td><td>0</td><td><span class=zero-hits>table.insert(timelines, data)</span></td></tr>
<tr><td>122</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>123</td><td><span class='text-muted'>N/A</span></td><td>-- Create a table to hold entity data for sorting</td>></tr>
<tr><td>124</td><td>0</td><td><span class=zero-hits>local entities = {}</span></td></tr>
<tr><td>125</td><td>0</td><td><span class=zero-hits>for i = ENTITY_RANGE, max_id do</span></td></tr>
<tr><td>126</td><td>0</td><td><span class=zero-hits>if dense_array[i] then</span></td></tr>
<tr><td>127</td><td>0</td><td><span class=zero-hits>local entity = dense_array[i]</span></td></tr>
<tr><td>128</td><td>0</td><td><span class=zero-hits>local id = ECS_ID(entity)</span></td></tr>
<tr><td>129</td><td><span class='text-muted'>N/A</span></td><td>-- Push entity and id into the new `entities` table</td>></tr>
<tr><td>130</td><td>0</td><td><span class=zero-hits>table.insert(entities, { entity = entity, id = id })</span></td></tr>
<tr><td>131</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>132</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>133</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>134</td><td><span class='text-muted'>N/A</span></td><td>-- Sort the entities by ECS_ID</td>></tr>
<tr><td>135</td><td>0</td><td><span class=zero-hits>table.sort(entities, function(a, b)</span></td></tr>
<tr><td>136</td><td>0</td><td><span class=zero-hits>return a.id < b.id</span></td></tr>
<tr><td>137</td><td><span class='text-muted'>N/A</span></td><td>end)</td>></tr>
<tr><td>138</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>139</td><td><span class='text-muted'>N/A</span></td><td>-- Print the sorted rows</td>></tr>
<tr><td>140</td><td>0</td><td><span class=zero-hits>for _, entity_data in ipairs(entities) do</span></td></tr>
<tr><td>141</td><td>0</td><td><span class=zero-hits>local entity = entity_data.entity</span></td></tr>
<tr><td>142</td><td>0</td><td><span class=zero-hits>local id = entity_data.id</span></td></tr>
<tr><td>143</td><td>0</td><td><span class=zero-hits>local status = "alive"</span></td></tr>
<tr><td>144</td><td>0</td><td><span class=zero-hits>if id > alive_count then</span></td></tr>
<tr><td>145</td><td>0</td><td><span class=zero-hits>status = "dead"</span></td></tr>
<tr><td>146</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>147</td><td>0</td><td><span class=zero-hits>local row = string.format("| %-" .. entity_column_width .. "s |", pe(entity))</span></td></tr>
<tr><td>148</td><td>0</td><td><span class=zero-hits>for j = 1, timeline do</span></td></tr>
<tr><td>149</td><td>0</td><td><span class=zero-hits>local timeline_data = timelines[j]</span></td></tr>
<tr><td>150</td><td>0</td><td><span class=zero-hits>local entity_data = timeline_data[id]</span></td></tr>
<tr><td>151</td><td>0</td><td><span class=zero-hits>if entity_data then</span></td></tr>
<tr><td>152</td><td>0</td><td><span class=zero-hits>row = row .. string.format(" %-" .. status_column_width .. "s |", entity_data)</span></td></tr>
<tr><td>153</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>154</td><td>0</td><td><span class=zero-hits>row = row .. string.format(" %-" .. status_column_width .. "s |", "-")</span></td></tr>
<tr><td>155</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>156</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>157</td><td>0</td><td><span class=zero-hits>print(row)</span></td></tr>
<tr><td>158</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>159</td><td>0</td><td><span class=zero-hits>print("-------------------------------------------------------------------")</span></td></tr>
<tr><td>160</td><td>0</td><td><span class=zero-hits>pad()</span></td></tr>
<tr><td>161</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>162</td><td>0</td><td><span class=zero-hits>local world_add = world.add</span></td></tr>
<tr><td>163</td><td>0</td><td><span class=zero-hits>local relations = {}</span></td></tr>
<tr><td>164</td><td>0</td><td><span class=zero-hits>w.add = function(self, entity: any, component: any)</span></td></tr>
<tr><td>165</td><td>0</td><td><span class=zero-hits>world_add(world, entity, component)</span></td></tr>
<tr><td>166</td><td>0</td><td><span class=zero-hits>if jecs.IS_PAIR(component) then</span></td></tr>
<tr><td>167</td><td>0</td><td><span class=zero-hits>local relation = jecs.pair_first(world, component)</span></td></tr>
<tr><td>168</td><td>0</td><td><span class=zero-hits>local target = jecs.pair_second(world, component)</span></td></tr>
<tr><td>169</td><td>0</td><td><span class=zero-hits>print(`*added ({pe(relation)}, {pe(target)}) to {pe(entity)}`)</span></td></tr>
<tr><td>170</td><td>0</td><td><span class=zero-hits>pad()</span></td></tr>
<tr><td>171</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>172</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>173</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>174</td><td>0</td><td><span class=zero-hits>local world_delete = world.delete</span></td></tr>
<tr><td>175</td><td>0</td><td><span class=zero-hits>w.delete = function(self, e)</span></td></tr>
<tr><td>176</td><td>0</td><td><span class=zero-hits>world_delete(world, e)</span></td></tr>
<tr><td>177</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>178</td><td>0</td><td><span class=zero-hits>local idr_t = component_index[pair(__, e)]</span></td></tr>
<tr><td>179</td><td>0</td><td><span class=zero-hits>if idr_t then</span></td></tr>
<tr><td>180</td><td>0</td><td><span class=zero-hits>for archetype_id in idr_t.cache do</span></td></tr>
<tr><td>181</td><td>0</td><td><span class=zero-hits>local archetype = world.archetypes[archetype_id]</span></td></tr>
<tr><td>182</td><td>0</td><td><span class=zero-hits>for _, id in archetype.types do</span></td></tr>
<tr><td>183</td><td>0</td><td><span class=zero-hits>if not jecs.IS_PAIR(id) then</span></td></tr>
<tr><td>184</td><td>0</td><td><span class=zero-hits>continue</span></td></tr>
<tr><td>185</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>186</td><td>0</td><td><span class=zero-hits>local object = jecs.pair_second(world, id)</span></td></tr>
<tr><td>187</td><td>0</td><td><span class=zero-hits>if object ~= e then</span></td></tr>
<tr><td>188</td><td>0</td><td><span class=zero-hits>continue</span></td></tr>
<tr><td>189</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>190</td><td>0</td><td><span class=zero-hits>local id_record = component_index[id]</span></td></tr>
<tr><td>191</td><td>0</td><td><span class=zero-hits>local flags = id_record.flags</span></td></tr>
<tr><td>192</td><td>0</td><td><span class=zero-hits>local flags_delete_mask: number = bit32.band(flags, jecs.ECS_ID_DELETE)</span></td></tr>
<tr><td>193</td><td>0</td><td><span class=zero-hits>if flags_delete_mask ~= 0 then</span></td></tr>
<tr><td>194</td><td>0</td><td><span class=zero-hits>for _, entity in archetype.entities do</span></td></tr>
<tr><td>195</td><td>0</td><td><span class=zero-hits>print(`*deleted dependant {pe(entity)} of {pe(e)}`)</span></td></tr>
<tr><td>196</td><td>0</td><td><span class=zero-hits>pad()</span></td></tr>
<tr><td>197</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>198</td><td>0</td><td><span class=zero-hits>break</span></td></tr>
<tr><td>199</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>200</td><td>0</td><td><span class=zero-hits>for _, entity in archetype.entities do</span></td></tr>
<tr><td>201</td><td>0</td><td><span class=zero-hits>print(</span></td></tr>
<tr><td>202</td><td>0</td><td><span class=zero-hits>`*removed dependency ({pe(jecs.pair_first(world, id))}, {pe(object)}) from {pe(entity)}`</span></td></tr>
<tr><td>203</td><td>0</td><td><span class=zero-hits>)</span></td></tr>
<tr><td>204</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>205</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>206</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>207</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>208</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>209</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>210</td><td>0</td><td><span class=zero-hits>print(`*deleted {pe(e)}`)</span></td></tr>
<tr><td>211</td><td>0</td><td><span class=zero-hits>pad()</span></td></tr>
<tr><td>212</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>213</td><td>0</td><td><span class=zero-hits>return w</span></td></tr>
<tr><td>214</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>215</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>216</td><td>1</td><td><span class=high-hits>return lifetime_tracker_add</span></td></tr>
</tbody></table></body></html>

617
coverage/testkit.luau.html vendored Normal file
View file

@ -0,0 +1,617 @@
<html><head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<style>
body { font-family: monospace; text-align: center; }
#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }
#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }
#funcTable th { background-color: #ddd; }
#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }
.zero-hits { background-color: #fcc; font-weight: bold; color: red; }
.nonzero-hits { color: green; font-weight: bold; }
.low-hits { background-color: #ffe6b3; }
.high-hits { background-color: #cfc; }
.source-code-table { margin-left: 10px; }th, td { padding: 0px; font-size: 12px; }
table.table { font-size: 14px; border-collapse: collapse; }
table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }
table.table tr { height: auto; }
</style></head><body>
<h1 class="text-center">testkit.luau Coverage</h1>
<h2>Total Execution Hits: 1826</h2>
<h2>Function Coverage Overview: 64.52%</h2>
<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">Toggle Function Coverage</button>
<div class="collapse show" id="funcTable">
<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>
<tr><td style="padding: 1px; min-width: 22ch;"><main></td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">white_underline:11</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">white:15</td><td style="padding: 1px; color: green; font-weight: bold;">24</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">green:19</td><td style="padding: 1px; color: green; font-weight: bold;">77</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">red:23</td><td style="padding: 1px; color: green; font-weight: bold;">146</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">yellow:27</td><td style="padding: 1px; color: green; font-weight: bold;">76</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">red_highlight:31</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">green_highlight:35</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">gray:39</td><td style="padding: 1px; color: green; font-weight: bold;">84</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">orange:43</td><td style="padding: 1px; color: green; font-weight: bold;">73</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">convert_units:48</td><td style="padding: 1px; color: green; font-weight: bold;">6</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">output_test_result:131</td><td style="padding: 1px; color: green; font-weight: bold;">24</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">CASE:169</td><td style="padding: 1px; color: green; font-weight: bold;">73</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">CHECK_EXPECT_ERR:183</td><td style="padding: 1px; color: green; font-weight: bold;">9</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">CHECK:201</td><td style="padding: 1px; color: green; font-weight: bold;">1195</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">TEST:224</td><td style="padding: 1px; color: green; font-weight: bold;">24</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">FOCUS:237</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">FINISH:248</td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;"><anonymous>:264</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">SKIP:314</td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">START:330</td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">BENCH:342</td><td style="padding: 1px; color: green; font-weight: bold;">3</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;"><anonymous>:354</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">round:372</td><td style="padding: 1px; color: green; font-weight: bold;">6</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">print2:396</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">tos:401</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">shallow_eq:480</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">deep_eq:500</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">test:533</td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">benchmark:545</td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
<tr><td style="padding: 1px; min-width: 22ch;">disable_formatting:549</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
</tbody></table></div>
<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>
<tr><td>1</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>2</td><td><span class='text-muted'>N/A</span></td><td>-- testkit.luau</td>></tr>
<tr><td>3</td><td><span class='text-muted'>N/A</span></td><td>-- v0.7.3</td>></tr>
<tr><td>4</td><td><span class='text-muted'>N/A</span></td><td>-- MIT License</td>></tr>
<tr><td>5</td><td><span class='text-muted'>N/A</span></td><td>-- Copyright (c) 2022 centau</td>></tr>
<tr><td>6</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>7</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>8</td><td>1</td><td><span class=low-hits>local disable_ansi = false</span></td></tr>
<tr><td>9</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>10</td><td>1</td><td><span class=low-hits>local color = {</span></td></tr>
<tr><td>11</td><td>1</td><td><span class=low-hits>white_underline = function(s: string): string</span></td></tr>
<tr><td>12</td><td>0</td><td><span class=zero-hits>return if disable_ansi then s else `\27[1;4m{s}\27[0m`</span></td></tr>
<tr><td>13</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>14</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>15</td><td>1</td><td><span class=low-hits>white = function(s: string): string</span></td></tr>
<tr><td>16</td><td>24</td><td><span class=low-hits>return if disable_ansi then s else `\27[37;1m{s}\27[0m`</span></td></tr>
<tr><td>17</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>18</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>19</td><td>1</td><td><span class=low-hits>green = function(s: string): string</span></td></tr>
<tr><td>20</td><td>77</td><td><span class=low-hits>return if disable_ansi then s else `\27[32;1m{s}\27[0m`</span></td></tr>
<tr><td>21</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>22</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>23</td><td>1</td><td><span class=low-hits>red = function(s: string): string</span></td></tr>
<tr><td>24</td><td>146</td><td><span class=low-hits>return if disable_ansi then s else `\27[31;1m{s}\27[0m`</span></td></tr>
<tr><td>25</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>26</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>27</td><td>1</td><td><span class=low-hits>yellow = function(s: string): string</span></td></tr>
<tr><td>28</td><td>76</td><td><span class=low-hits>return if disable_ansi then s else `\27[33;1m{s}\27[0m`</span></td></tr>
<tr><td>29</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>30</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>31</td><td>1</td><td><span class=low-hits>red_highlight = function(s: string): string</span></td></tr>
<tr><td>32</td><td>0</td><td><span class=zero-hits>return if disable_ansi then s else `\27[41;1;30m{s}\27[0m`</span></td></tr>
<tr><td>33</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>34</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>35</td><td>1</td><td><span class=low-hits>green_highlight = function(s: string): string</span></td></tr>
<tr><td>36</td><td>0</td><td><span class=zero-hits>return if disable_ansi then s else `\27[42;1;30m{s}\27[0m`</span></td></tr>
<tr><td>37</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>38</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>39</td><td>1</td><td><span class=low-hits>gray = function(s: string): string</span></td></tr>
<tr><td>40</td><td>84</td><td><span class=low-hits>return if disable_ansi then s else `\27[38;1m{s}\27[0m`</span></td></tr>
<tr><td>41</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>42</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>43</td><td>1</td><td><span class=low-hits>orange = function(s: string): string</span></td></tr>
<tr><td>44</td><td>73</td><td><span class=low-hits>return if disable_ansi then s else `\27[38;5;208m{s}\27[0m`</span></td></tr>
<tr><td>45</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>46</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>47</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>48</td><td>1</td><td><span class=low-hits>local function convert_units(unit: string, value: number): (number, string)</span></td></tr>
<tr><td>49</td><td>6</td><td><span class=low-hits>local sign = math.sign(value)</span></td></tr>
<tr><td>50</td><td>6</td><td><span class=low-hits>value = math.abs(value)</span></td></tr>
<tr><td>51</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>52</td><td>6</td><td><span class=low-hits>local prefix_colors = {</span></td></tr>
<tr><td>53</td><td>6</td><td><span class=low-hits>[4] = color.red,</span></td></tr>
<tr><td>54</td><td>6</td><td><span class=low-hits>[3] = color.red,</span></td></tr>
<tr><td>55</td><td>6</td><td><span class=low-hits>[2] = color.yellow,</span></td></tr>
<tr><td>56</td><td>6</td><td><span class=low-hits>[1] = color.yellow,</span></td></tr>
<tr><td>57</td><td>6</td><td><span class=low-hits>[0] = color.green,</span></td></tr>
<tr><td>58</td><td>6</td><td><span class=low-hits>[-1] = color.red,</span></td></tr>
<tr><td>59</td><td>6</td><td><span class=low-hits>[-2] = color.yellow,</span></td></tr>
<tr><td>60</td><td>6</td><td><span class=low-hits>[-3] = color.green,</span></td></tr>
<tr><td>61</td><td>6</td><td><span class=low-hits>[-4] = color.red,</span></td></tr>
<tr><td>62</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>63</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>64</td><td>6</td><td><span class=low-hits>local prefixes = {</span></td></tr>
<tr><td>65</td><td>6</td><td><span class=low-hits>[4] = "T",</span></td></tr>
<tr><td>66</td><td>6</td><td><span class=low-hits>[3] = "G",</span></td></tr>
<tr><td>67</td><td>6</td><td><span class=low-hits>[2] = "M",</span></td></tr>
<tr><td>68</td><td>6</td><td><span class=low-hits>[1] = "k",</span></td></tr>
<tr><td>69</td><td>6</td><td><span class=low-hits>[0] = " ",</span></td></tr>
<tr><td>70</td><td>6</td><td><span class=low-hits>[-1] = "m",</span></td></tr>
<tr><td>71</td><td>6</td><td><span class=low-hits>[-2] = "u",</span></td></tr>
<tr><td>72</td><td>6</td><td><span class=low-hits>[-3] = "n",</span></td></tr>
<tr><td>73</td><td>6</td><td><span class=low-hits>[-4] = "p",</span></td></tr>
<tr><td>74</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>75</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>76</td><td>6</td><td><span class=low-hits>local order = 0</span></td></tr>
<tr><td>77</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>78</td><td>7</td><td><span class=low-hits>while value >= 1000 do</span></td></tr>
<tr><td>79</td><td>1</td><td><span class=low-hits>order += 1</span></td></tr>
<tr><td>80</td><td>1</td><td><span class=low-hits>value /= 1000</span></td></tr>
<tr><td>81</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>82</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>83</td><td>11</td><td><span class=low-hits>while value ~= 0 and value < 1 do</span></td></tr>
<tr><td>84</td><td>7</td><td><span class=low-hits>order -= 1</span></td></tr>
<tr><td>85</td><td>7</td><td><span class=low-hits>value *= 1000</span></td></tr>
<tr><td>86</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>87</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>88</td><td>6</td><td><span class=low-hits>if value >= 100 then</span></td></tr>
<tr><td>89</td><td>1</td><td><span class=low-hits>value = math.floor(value)</span></td></tr>
<tr><td>90</td><td>5</td><td><span class=low-hits>elseif value >= 10 then</span></td></tr>
<tr><td>91</td><td>2</td><td><span class=low-hits>value = math.floor(value * 1e1) / 1e1</span></td></tr>
<tr><td>92</td><td>3</td><td><span class=low-hits>elseif value >= 1 then</span></td></tr>
<tr><td>93</td><td>1</td><td><span class=low-hits>value = math.floor(value * 1e2) / 1e2</span></td></tr>
<tr><td>94</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>95</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>96</td><td>6</td><td><span class=low-hits>return value * sign, prefix_colors[order](prefixes[order] .. unit)</span></td></tr>
<tr><td>97</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>98</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>99</td><td>1</td><td><span class=low-hits>local WALL = color.gray("│")</span></td></tr>
<tr><td>100</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>101</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>102</td><td><span class='text-muted'>N/A</span></td><td>-- Testing</td>></tr>
<tr><td>103</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>104</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>105</td><td>0</td><td><span class=zero-hits>type Test = {</span></td></tr>
<tr><td>106</td><td>0</td><td><span class=zero-hits>name: string,</span></td></tr>
<tr><td>107</td><td>0</td><td><span class=zero-hits>case: Case?,</span></td></tr>
<tr><td>108</td><td>0</td><td><span class=zero-hits>cases: { Case },</span></td></tr>
<tr><td>109</td><td>0</td><td><span class=zero-hits>duration: number,</span></td></tr>
<tr><td>110</td><td>0</td><td><span class=zero-hits>error: {</span></td></tr>
<tr><td>111</td><td>0</td><td><span class=zero-hits>message: string,</span></td></tr>
<tr><td>112</td><td>0</td><td><span class=zero-hits>trace: string,</span></td></tr>
<tr><td>113</td><td>0</td><td><span class=zero-hits>}?,</span></td></tr>
<tr><td>114</td><td>0</td><td><span class=zero-hits>focus: boolean,</span></td></tr>
<tr><td>115</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>116</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>117</td><td>0</td><td><span class=zero-hits>type Case = {</span></td></tr>
<tr><td>118</td><td>0</td><td><span class=zero-hits>name: string,</span></td></tr>
<tr><td>119</td><td>0</td><td><span class=zero-hits>result: number,</span></td></tr>
<tr><td>120</td><td>0</td><td><span class=zero-hits>line: number?,</span></td></tr>
<tr><td>121</td><td>0</td><td><span class=zero-hits>focus: boolean,</span></td></tr>
<tr><td>122</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>123</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>124</td><td>1</td><td><span class=low-hits>local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5</span></td></tr>
<tr><td>125</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>126</td><td>1</td><td><span class=low-hits>local check_for_focused = false</span></td></tr>
<tr><td>127</td><td>1</td><td><span class=low-hits>local skip = false</span></td></tr>
<tr><td>128</td><td>1</td><td><span class=low-hits>local test: Test?</span></td></tr>
<tr><td>129</td><td>1</td><td><span class=low-hits>local tests: { Test } = {}</span></td></tr>
<tr><td>130</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>131</td><td>1</td><td><span class=low-hits>local function output_test_result(test: Test)</span></td></tr>
<tr><td>132</td><td>24</td><td><span class=low-hits>if check_for_focused then</span></td></tr>
<tr><td>133</td><td>0</td><td><span class=zero-hits>local any_focused = test.focus</span></td></tr>
<tr><td>134</td><td>0</td><td><span class=zero-hits>for _, case in test.cases do</span></td></tr>
<tr><td>135</td><td>0</td><td><span class=zero-hits>any_focused = any_focused or case.focus</span></td></tr>
<tr><td>136</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>137</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>138</td><td>0</td><td><span class=zero-hits>if not any_focused then</span></td></tr>
<tr><td>139</td><td>0</td><td><span class=zero-hits>return</span></td></tr>
<tr><td>140</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>141</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>142</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>143</td><td>24</td><td><span class=low-hits>print(color.white(test.name))</span></td></tr>
<tr><td>144</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>145</td><td>24</td><td><span class=low-hits>for _, case in test.cases do</span></td></tr>
<tr><td>146</td><td>73</td><td><span class=low-hits>local status = ({</span></td></tr>
<tr><td>147</td><td>73</td><td><span class=low-hits>[PASS] = color.green("PASS"),</span></td></tr>
<tr><td>148</td><td>73</td><td><span class=low-hits>[FAIL] = color.red("FAIL"),</span></td></tr>
<tr><td>149</td><td>73</td><td><span class=low-hits>[NONE] = color.orange("NONE"),</span></td></tr>
<tr><td>150</td><td>73</td><td><span class=low-hits>[ERROR] = color.red("FAIL"),</span></td></tr>
<tr><td>151</td><td>73</td><td><span class=low-hits>[SKIPPED] = color.yellow("SKIP"),</span></td></tr>
<tr><td>152</td><td>73</td><td><span class=low-hits>})[case.result]</span></td></tr>
<tr><td>153</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>154</td><td>73</td><td><span class=low-hits>local line = case.result == FAIL and color.red(`{case.line}:`) or ""</span></td></tr>
<tr><td>155</td><td>73</td><td><span class=low-hits>if check_for_focused and case.focus == false and test.focus == false then</span></td></tr>
<tr><td>156</td><td>0</td><td><span class=zero-hits>continue</span></td></tr>
<tr><td>157</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>158</td><td>73</td><td><span class=low-hits>print(`{status}{WALL} {line}{color.gray(case.name)}`)</span></td></tr>
<tr><td>159</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>160</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>161</td><td>24</td><td><span class=low-hits>if test.error then</span></td></tr>
<tr><td>162</td><td>0</td><td><span class=zero-hits>print(color.gray("error: ") .. color.red(test.error.message))</span></td></tr>
<tr><td>163</td><td>0</td><td><span class=zero-hits>print(color.gray("trace: ") .. color.red(test.error.trace))</span></td></tr>
<tr><td>164</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>165</td><td>24</td><td><span class=low-hits>print()</span></td></tr>
<tr><td>166</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>167</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>168</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>169</td><td>1</td><td><span class=low-hits>local function CASE(name: string)</span></td></tr>
<tr><td>170</td><td>73</td><td><span class=low-hits>skip = false</span></td></tr>
<tr><td>171</td><td>73</td><td><span class=low-hits>assert(test, "no active test")</span></td></tr>
<tr><td>172</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>173</td><td>73</td><td><span class=low-hits>local case = {</span></td></tr>
<tr><td>174</td><td>73</td><td><span class=low-hits>name = name,</span></td></tr>
<tr><td>175</td><td>73</td><td><span class=low-hits>result = NONE,</span></td></tr>
<tr><td>176</td><td>73</td><td><span class=low-hits>focus = false,</span></td></tr>
<tr><td>177</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>178</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>179</td><td>73</td><td><span class=low-hits>test.case = case</span></td></tr>
<tr><td>180</td><td>73</td><td><span class=low-hits>table.insert(test.cases, case)</span></td></tr>
<tr><td>181</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>182</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>183</td><td>1</td><td><span class=low-hits>local function CHECK_EXPECT_ERR(fn, ...)</span></td></tr>
<tr><td>184</td><td>9</td><td><span class=low-hits>assert(test, "no active test")</span></td></tr>
<tr><td>185</td><td>9</td><td><span class=low-hits>local case = test.case</span></td></tr>
<tr><td>186</td><td>9</td><td><span class=low-hits>if not case then</span></td></tr>
<tr><td>187</td><td>0</td><td><span class=zero-hits>CASE("")</span></td></tr>
<tr><td>188</td><td>0</td><td><span class=zero-hits>case = test.case</span></td></tr>
<tr><td>189</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>190</td><td>9</td><td><span class=low-hits>assert(case, "no active case")</span></td></tr>
<tr><td>191</td><td>9</td><td><span class=low-hits>if case.result ~= FAIL then</span></td></tr>
<tr><td>192</td><td>9</td><td><span class=low-hits>local ok, err = pcall(fn, ...)</span></td></tr>
<tr><td>193</td><td>9</td><td><span class=low-hits>case.result = if ok then FAIL else PASS</span></td></tr>
<tr><td>194</td><td>9</td><td><span class=low-hits>if skip then</span></td></tr>
<tr><td>195</td><td>0</td><td><span class=zero-hits>case.result = SKIPPED</span></td></tr>
<tr><td>196</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>197</td><td>9</td><td><span class=low-hits>case.line = debug.info(stack and stack + 1 or 2, "l")</span></td></tr>
<tr><td>198</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>199</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>200</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>201</td><td>1</td><td><span class=low-hits>local function CHECK<T>(value: T, stack: number?): T?</span></td></tr>
<tr><td>202</td><td>1195</td><td><span class=high-hits>assert(test, "no active test")</span></td></tr>
<tr><td>203</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>204</td><td>1195</td><td><span class=high-hits>local case = test.case</span></td></tr>
<tr><td>205</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>206</td><td>1195</td><td><span class=high-hits>if not case then</span></td></tr>
<tr><td>207</td><td>9</td><td><span class=low-hits>CASE("")</span></td></tr>
<tr><td>208</td><td>9</td><td><span class=low-hits>case = test.case</span></td></tr>
<tr><td>209</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>210</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>211</td><td>1195</td><td><span class=high-hits>assert(case, "no active case")</span></td></tr>
<tr><td>212</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>213</td><td>1195</td><td><span class=high-hits>if case.result ~= FAIL then</span></td></tr>
<tr><td>214</td><td>1195</td><td><span class=high-hits>case.result = value and PASS or FAIL</span></td></tr>
<tr><td>215</td><td>1195</td><td><span class=high-hits>if skip then</span></td></tr>
<tr><td>216</td><td>1</td><td><span class=low-hits>case.result = SKIPPED</span></td></tr>
<tr><td>217</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>218</td><td>1195</td><td><span class=high-hits>case.line = debug.info(stack and stack + 1 or 2, "l")</span></td></tr>
<tr><td>219</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>220</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>221</td><td>1195</td><td><span class=high-hits>return value</span></td></tr>
<tr><td>222</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>223</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>224</td><td>1</td><td><span class=low-hits>local function TEST(name: string, fn: () -> ())</span></td></tr>
<tr><td>225</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>226</td><td>24</td><td><span class=low-hits>test = {</span></td></tr>
<tr><td>227</td><td>24</td><td><span class=low-hits>name = name,</span></td></tr>
<tr><td>228</td><td>24</td><td><span class=low-hits>cases = {},</span></td></tr>
<tr><td>229</td><td>24</td><td><span class=low-hits>duration = 0,</span></td></tr>
<tr><td>230</td><td>24</td><td><span class=low-hits>focus = false,</span></td></tr>
<tr><td>231</td><td>24</td><td><span class=low-hits>fn = fn</span></td></tr>
<tr><td>232</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>233</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>234</td><td>24</td><td><span class=low-hits>table.insert(tests, test)</span></td></tr>
<tr><td>235</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>236</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>237</td><td>1</td><td><span class=low-hits>local function FOCUS()</span></td></tr>
<tr><td>238</td><td>0</td><td><span class=zero-hits>assert(test, "no active test")</span></td></tr>
<tr><td>239</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>240</td><td>0</td><td><span class=zero-hits>check_for_focused = true</span></td></tr>
<tr><td>241</td><td>0</td><td><span class=zero-hits>if test.case then</span></td></tr>
<tr><td>242</td><td>0</td><td><span class=zero-hits>test.case.focus = true</span></td></tr>
<tr><td>243</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>244</td><td>0</td><td><span class=zero-hits>test.focus = true</span></td></tr>
<tr><td>245</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>246</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>247</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>248</td><td>1</td><td><span class=low-hits>local function FINISH(): boolean</span></td></tr>
<tr><td>249</td><td>1</td><td><span class=low-hits>local success = true</span></td></tr>
<tr><td>250</td><td>1</td><td><span class=low-hits>local total_cases = 0</span></td></tr>
<tr><td>251</td><td>1</td><td><span class=low-hits>local passed_cases = 0</span></td></tr>
<tr><td>252</td><td>1</td><td><span class=low-hits>local passed_focus_cases = 0</span></td></tr>
<tr><td>253</td><td>1</td><td><span class=low-hits>local total_focus_cases = 0</span></td></tr>
<tr><td>254</td><td>1</td><td><span class=low-hits>local duration = 0</span></td></tr>
<tr><td>255</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>256</td><td>1</td><td><span class=low-hits>for _, t in tests do</span></td></tr>
<tr><td>257</td><td>24</td><td><span class=low-hits>if check_for_focused and not t.focus then</span></td></tr>
<tr><td>258</td><td>0</td><td><span class=zero-hits>continue</span></td></tr>
<tr><td>259</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>260</td><td>24</td><td><span class=low-hits>test = t</span></td></tr>
<tr><td>261</td><td>24</td><td><span class=low-hits>fn = t.fn</span></td></tr>
<tr><td>262</td><td>24</td><td><span class=low-hits>local start = os.clock()</span></td></tr>
<tr><td>263</td><td>24</td><td><span class=low-hits>local err</span></td></tr>
<tr><td>264</td><td>24</td><td><span class=low-hits>local success = xpcall(fn, function(m: string)</span></td></tr>
<tr><td>265</td><td>0</td><td><span class=zero-hits>err = { message = m, trace = debug.traceback(nil, 2) }</span></td></tr>
<tr><td>266</td><td><span class='text-muted'>N/A</span></td><td>end)</td>></tr>
<tr><td>267</td><td>24</td><td><span class=low-hits>test.duration = os.clock() - start</span></td></tr>
<tr><td>268</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>269</td><td>24</td><td><span class=low-hits>if not test.case then</span></td></tr>
<tr><td>270</td><td>0</td><td><span class=zero-hits>CASE("")</span></td></tr>
<tr><td>271</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>272</td><td>24</td><td><span class=low-hits>assert(test.case, "no active case")</span></td></tr>
<tr><td>273</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>274</td><td>24</td><td><span class=low-hits>if not success then</span></td></tr>
<tr><td>275</td><td>0</td><td><span class=zero-hits>test.case.result = ERROR</span></td></tr>
<tr><td>276</td><td>0</td><td><span class=zero-hits>test.error = err</span></td></tr>
<tr><td>277</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>278</td><td>24</td><td><span class=low-hits>collectgarbage()</span></td></tr>
<tr><td>279</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>280</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>281</td><td>1</td><td><span class=low-hits>for _, test in tests do</span></td></tr>
<tr><td>282</td><td>24</td><td><span class=low-hits>duration += test.duration</span></td></tr>
<tr><td>283</td><td>24</td><td><span class=low-hits>for _, case in test.cases do</span></td></tr>
<tr><td>284</td><td>73</td><td><span class=low-hits>total_cases += 1</span></td></tr>
<tr><td>285</td><td>73</td><td><span class=low-hits>if case.focus or test.focus then</span></td></tr>
<tr><td>286</td><td>0</td><td><span class=zero-hits>total_focus_cases += 1</span></td></tr>
<tr><td>287</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>288</td><td>73</td><td><span class=low-hits>if case.result == PASS or case.result == NONE or case.result == SKIPPED then</span></td></tr>
<tr><td>289</td><td>73</td><td><span class=low-hits>if case.focus or test.focus then</span></td></tr>
<tr><td>290</td><td>0</td><td><span class=zero-hits>passed_focus_cases += 1</span></td></tr>
<tr><td>291</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>292</td><td>73</td><td><span class=low-hits>passed_cases += 1</span></td></tr>
<tr><td>293</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>294</td><td>0</td><td><span class=zero-hits>success = false</span></td></tr>
<tr><td>295</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>296</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>297</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>298</td><td>24</td><td><span class=low-hits>output_test_result(test)</span></td></tr>
<tr><td>299</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>300</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>301</td><td>1</td><td><span class=low-hits>print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3)))</span></td></tr>
<tr><td>302</td><td>1</td><td><span class=low-hits>if check_for_focused then</span></td></tr>
<tr><td>303</td><td>0</td><td><span class=zero-hits>print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`))</span></td></tr>
<tr><td>304</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>305</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>306</td><td>1</td><td><span class=low-hits>local fails = total_cases - passed_cases</span></td></tr>
<tr><td>307</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>308</td><td>1</td><td><span class=low-hits>print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`))</span></td></tr>
<tr><td>309</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>310</td><td>1</td><td><span class=low-hits>check_for_focused = false</span></td></tr>
<tr><td>311</td><td>1</td><td><span class=low-hits>return success, table.clear(tests)</span></td></tr>
<tr><td>312</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>313</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>314</td><td>1</td><td><span class=low-hits>local function SKIP()</span></td></tr>
<tr><td>315</td><td>1</td><td><span class=low-hits>skip = true</span></td></tr>
<tr><td>316</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>317</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>318</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>319</td><td><span class='text-muted'>N/A</span></td><td>-- Benchmarking</td>></tr>
<tr><td>320</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>321</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>322</td><td>0</td><td><span class=zero-hits>type Bench = {</span></td></tr>
<tr><td>323</td><td>0</td><td><span class=zero-hits>time_start: number?,</span></td></tr>
<tr><td>324</td><td>0</td><td><span class=zero-hits>memory_start: number?,</span></td></tr>
<tr><td>325</td><td>0</td><td><span class=zero-hits>iterations: number?,</span></td></tr>
<tr><td>326</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>327</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>328</td><td>1</td><td><span class=low-hits>local bench: Bench?</span></td></tr>
<tr><td>329</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>330</td><td>1</td><td><span class=low-hits>function START(iter: number?): number</span></td></tr>
<tr><td>331</td><td>1</td><td><span class=low-hits>local n = iter or 1</span></td></tr>
<tr><td>332</td><td>1</td><td><span class=low-hits>assert(n > 0, "iterations must be greater than 0")</span></td></tr>
<tr><td>333</td><td>1</td><td><span class=low-hits>assert(bench, "no active benchmark")</span></td></tr>
<tr><td>334</td><td>1</td><td><span class=low-hits>assert(not bench.time_start, "clock was already started")</span></td></tr>
<tr><td>335</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>336</td><td>1</td><td><span class=low-hits>bench.iterations = n</span></td></tr>
<tr><td>337</td><td>1</td><td><span class=low-hits>bench.memory_start = gcinfo()</span></td></tr>
<tr><td>338</td><td>1</td><td><span class=low-hits>bench.time_start = os.clock()</span></td></tr>
<tr><td>339</td><td>1</td><td><span class=low-hits>return n</span></td></tr>
<tr><td>340</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>341</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>342</td><td>1</td><td><span class=low-hits>local function BENCH(name: string, fn: () -> ())</span></td></tr>
<tr><td>343</td><td>3</td><td><span class=low-hits>local active = bench</span></td></tr>
<tr><td>344</td><td>3</td><td><span class=low-hits>assert(not active, "a benchmark is already in progress")</span></td></tr>
<tr><td>345</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>346</td><td>3</td><td><span class=low-hits>bench = {}</span></td></tr>
<tr><td>347</td><td>3</td><td><span class=low-hits>assert(bench);</span></td></tr>
<tr><td>348</td><td>3</td><td><span class=low-hits>(collectgarbage :: any)("collect")</span></td></tr>
<tr><td>349</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>350</td><td>3</td><td><span class=low-hits>local mem_start = gcinfo()</span></td></tr>
<tr><td>351</td><td>3</td><td><span class=low-hits>local time_start = os.clock()</span></td></tr>
<tr><td>352</td><td>3</td><td><span class=low-hits>local err_msg: string?</span></td></tr>
<tr><td>353</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>354</td><td>3</td><td><span class=low-hits>local success = xpcall(fn, function(m: string)</span></td></tr>
<tr><td>355</td><td>0</td><td><span class=zero-hits>err_msg = m .. debug.traceback(nil, 2)</span></td></tr>
<tr><td>356</td><td><span class='text-muted'>N/A</span></td><td>end)</td>></tr>
<tr><td>357</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>358</td><td>3</td><td><span class=low-hits>local time_stop = os.clock()</span></td></tr>
<tr><td>359</td><td>3</td><td><span class=low-hits>local mem_stop = gcinfo()</span></td></tr>
<tr><td>360</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>361</td><td>3</td><td><span class=low-hits>if not success then</span></td></tr>
<tr><td>362</td><td>0</td><td><span class=zero-hits>print(`{WALL}{color.red("ERROR")}{WALL} {name}`)</span></td></tr>
<tr><td>363</td><td>0</td><td><span class=zero-hits>print(color.gray(err_msg :: string))</span></td></tr>
<tr><td>364</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>365</td><td>3</td><td><span class=low-hits>time_start = bench.time_start or time_start</span></td></tr>
<tr><td>366</td><td>3</td><td><span class=low-hits>mem_start = bench.memory_start or mem_start</span></td></tr>
<tr><td>367</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>368</td><td>3</td><td><span class=low-hits>local n = bench.iterations or 1</span></td></tr>
<tr><td>369</td><td>3</td><td><span class=low-hits>local d, d_unit = convert_units("s", (time_stop - time_start) / n)</span></td></tr>
<tr><td>370</td><td>3</td><td><span class=low-hits>local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))</span></td></tr>
<tr><td>371</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>372</td><td>3</td><td><span class=low-hits>local function round(x: number): string</span></td></tr>
<tr><td>373</td><td>6</td><td><span class=low-hits>return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x)</span></td></tr>
<tr><td>374</td><td>6</td><td><span class=low-hits>or string.format("%3.f", x)</span></td></tr>
<tr><td>375</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>376</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>377</td><td>3</td><td><span class=low-hits>print(</span></td></tr>
<tr><td>378</td><td>3</td><td><span class=low-hits>string.format(</span></td></tr>
<tr><td>379</td><td>3</td><td><span class=low-hits>`%s %s %s %s{WALL} %s`,</span></td></tr>
<tr><td>380</td><td>3</td><td><span class=low-hits>color.gray(round(d)),</span></td></tr>
<tr><td>381</td><td>3</td><td><span class=low-hits>d_unit,</span></td></tr>
<tr><td>382</td><td>3</td><td><span class=low-hits>color.gray(round(a)),</span></td></tr>
<tr><td>383</td><td>3</td><td><span class=low-hits>a_unit,</span></td></tr>
<tr><td>384</td><td>3</td><td><span class=low-hits>color.gray(name)</span></td></tr>
<tr><td>385</td><td>0</td><td><span class=zero-hits>)</span></td></tr>
<tr><td>386</td><td>0</td><td><span class=zero-hits>)</span></td></tr>
<tr><td>387</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>388</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>389</td><td>3</td><td><span class=low-hits>bench = nil</span></td></tr>
<tr><td>390</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>391</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>392</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>393</td><td><span class='text-muted'>N/A</span></td><td>-- Printing</td>></tr>
<tr><td>394</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>395</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>396</td><td>1</td><td><span class=low-hits>local function print2(v: unknown)</span></td></tr>
<tr><td>397</td><td>0</td><td><span class=zero-hits>type Buffer = { n: number, [number]: string }</span></td></tr>
<tr><td>398</td><td>0</td><td><span class=zero-hits>type Cyclic = { n: number, [{}]: number }</span></td></tr>
<tr><td>399</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>400</td><td><span class='text-muted'>N/A</span></td><td>-- overkill concatenationless string buffer</td>></tr>
<tr><td>401</td><td>0</td><td><span class=zero-hits>local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)</span></td></tr>
<tr><td>402</td><td>0</td><td><span class=zero-hits>local TAB = " "</span></td></tr>
<tr><td>403</td><td>0</td><td><span class=zero-hits>local indent = table.concat(table.create(stack, TAB))</span></td></tr>
<tr><td>404</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>405</td><td>0</td><td><span class=zero-hits>if type(value) == "string" then</span></td></tr>
<tr><td>406</td><td>0</td><td><span class=zero-hits>local n = str.n</span></td></tr>
<tr><td>407</td><td>0</td><td><span class=zero-hits>str[n + 1] = "\""</span></td></tr>
<tr><td>408</td><td>0</td><td><span class=zero-hits>str[n + 2] = value</span></td></tr>
<tr><td>409</td><td>0</td><td><span class=zero-hits>str[n + 3] = "\""</span></td></tr>
<tr><td>410</td><td>0</td><td><span class=zero-hits>str.n = n + 3</span></td></tr>
<tr><td>411</td><td>0</td><td><span class=zero-hits>elseif type(value) ~= "table" then</span></td></tr>
<tr><td>412</td><td>0</td><td><span class=zero-hits>local n = str.n</span></td></tr>
<tr><td>413</td><td>0</td><td><span class=zero-hits>str[n + 1] = value == nil and "nil" or tostring(value)</span></td></tr>
<tr><td>414</td><td>0</td><td><span class=zero-hits>str.n = n + 1</span></td></tr>
<tr><td>415</td><td>0</td><td><span class=zero-hits>elseif next(value) == nil then</span></td></tr>
<tr><td>416</td><td>0</td><td><span class=zero-hits>local n = str.n</span></td></tr>
<tr><td>417</td><td>0</td><td><span class=zero-hits>str[n + 1] = "{}"</span></td></tr>
<tr><td>418</td><td>0</td><td><span class=zero-hits>str.n = n + 1</span></td></tr>
<tr><td>419</td><td>0</td><td><span class=zero-hits>else -- is table</span></td></tr>
<tr><td>420</td><td>0</td><td><span class=zero-hits>local tabbed_indent = indent .. TAB</span></td></tr>
<tr><td>421</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>422</td><td>0</td><td><span class=zero-hits>if cyclic[value] then</span></td></tr>
<tr><td>423</td><td>0</td><td><span class=zero-hits>str.n += 1</span></td></tr>
<tr><td>424</td><td>0</td><td><span class=zero-hits>str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)</span></td></tr>
<tr><td>425</td><td>0</td><td><span class=zero-hits>return</span></td></tr>
<tr><td>426</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>427</td><td>0</td><td><span class=zero-hits>cyclic.n += 1</span></td></tr>
<tr><td>428</td><td>0</td><td><span class=zero-hits>cyclic[value] = cyclic.n</span></td></tr>
<tr><td>429</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>430</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>431</td><td>0</td><td><span class=zero-hits>str.n += 3</span></td></tr>
<tr><td>432</td><td>0</td><td><span class=zero-hits>str[str.n - 2] = "{ "</span></td></tr>
<tr><td>433</td><td>0</td><td><span class=zero-hits>str[str.n - 1] = color.gray(tostring(cyclic[value]))</span></td></tr>
<tr><td>434</td><td>0</td><td><span class=zero-hits>str[str.n - 0] = "\n"</span></td></tr>
<tr><td>435</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>436</td><td>0</td><td><span class=zero-hits>local i, v = next(value, nil)</span></td></tr>
<tr><td>437</td><td>0</td><td><span class=zero-hits>while v ~= nil do</span></td></tr>
<tr><td>438</td><td>0</td><td><span class=zero-hits>local n = str.n</span></td></tr>
<tr><td>439</td><td>0</td><td><span class=zero-hits>str[n + 1] = tabbed_indent</span></td></tr>
<tr><td>440</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>441</td><td>0</td><td><span class=zero-hits>if type(i) ~= "string" then</span></td></tr>
<tr><td>442</td><td>0</td><td><span class=zero-hits>str[n + 2] = "["</span></td></tr>
<tr><td>443</td><td>0</td><td><span class=zero-hits>str[n + 3] = tostring(i)</span></td></tr>
<tr><td>444</td><td>0</td><td><span class=zero-hits>str[n + 4] = "]"</span></td></tr>
<tr><td>445</td><td>0</td><td><span class=zero-hits>n += 4</span></td></tr>
<tr><td>446</td><td>0</td><td><span class=zero-hits>else</span></td></tr>
<tr><td>447</td><td>0</td><td><span class=zero-hits>str[n + 2] = tostring(i)</span></td></tr>
<tr><td>448</td><td>0</td><td><span class=zero-hits>n += 2</span></td></tr>
<tr><td>449</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>450</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>451</td><td>0</td><td><span class=zero-hits>str[n + 1] = " = "</span></td></tr>
<tr><td>452</td><td>0</td><td><span class=zero-hits>str.n = n + 1</span></td></tr>
<tr><td>453</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>454</td><td>0</td><td><span class=zero-hits>tos(v, stack + 1, str, cyclic)</span></td></tr>
<tr><td>455</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>456</td><td>0</td><td><span class=zero-hits>i, v = next(value, i)</span></td></tr>
<tr><td>457</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>458</td><td>0</td><td><span class=zero-hits>n = str.n</span></td></tr>
<tr><td>459</td><td>0</td><td><span class=zero-hits>str[n + 1] = v ~= nil and ",\n" or "\n"</span></td></tr>
<tr><td>460</td><td>0</td><td><span class=zero-hits>str.n = n + 1</span></td></tr>
<tr><td>461</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>462</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>463</td><td>0</td><td><span class=zero-hits>local n = str.n</span></td></tr>
<tr><td>464</td><td>0</td><td><span class=zero-hits>str[n + 1] = indent</span></td></tr>
<tr><td>465</td><td>0</td><td><span class=zero-hits>str[n + 2] = "}"</span></td></tr>
<tr><td>466</td><td>0</td><td><span class=zero-hits>str.n = n + 2</span></td></tr>
<tr><td>467</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>468</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>469</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>470</td><td>0</td><td><span class=zero-hits>local str = { n = 0 }</span></td></tr>
<tr><td>471</td><td>0</td><td><span class=zero-hits>local cyclic = { n = 0 }</span></td></tr>
<tr><td>472</td><td>0</td><td><span class=zero-hits>tos(v, 0, str, cyclic)</span></td></tr>
<tr><td>473</td><td>0</td><td><span class=zero-hits>print(table.concat(str))</span></td></tr>
<tr><td>474</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>475</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>476</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>477</td><td><span class='text-muted'>N/A</span></td><td>-- Equality</td>></tr>
<tr><td>478</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>479</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>480</td><td>1</td><td><span class=low-hits>local function shallow_eq(a: {}, b: {}): boolean</span></td></tr>
<tr><td>481</td><td>0</td><td><span class=zero-hits>if #a ~= #b then</span></td></tr>
<tr><td>482</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>483</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>484</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>485</td><td>0</td><td><span class=zero-hits>for i, v in next, a do</span></td></tr>
<tr><td>486</td><td>0</td><td><span class=zero-hits>if b[i] ~= v then</span></td></tr>
<tr><td>487</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>488</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>489</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>490</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>491</td><td>0</td><td><span class=zero-hits>for i, v in next, b do</span></td></tr>
<tr><td>492</td><td>0</td><td><span class=zero-hits>if a[i] ~= v then</span></td></tr>
<tr><td>493</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>494</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>495</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>496</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>497</td><td>0</td><td><span class=zero-hits>return true</span></td></tr>
<tr><td>498</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>499</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>500</td><td>1</td><td><span class=low-hits>local function deep_eq(a: {}, b: {}): boolean</span></td></tr>
<tr><td>501</td><td>0</td><td><span class=zero-hits>if #a ~= #b then</span></td></tr>
<tr><td>502</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>503</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>504</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>505</td><td>0</td><td><span class=zero-hits>for i, v in next, a do</span></td></tr>
<tr><td>506</td><td>0</td><td><span class=zero-hits>if type(b[i]) == "table" and type(v) == "table" then</span></td></tr>
<tr><td>507</td><td>0</td><td><span class=zero-hits>if deep_eq(b[i], v) == false then</span></td></tr>
<tr><td>508</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>509</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>510</td><td>0</td><td><span class=zero-hits>elseif b[i] ~= v then</span></td></tr>
<tr><td>511</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>512</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>513</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>514</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>515</td><td>0</td><td><span class=zero-hits>for i, v in next, b do</span></td></tr>
<tr><td>516</td><td>0</td><td><span class=zero-hits>if type(a[i]) == "table" and type(v) == "table" then</span></td></tr>
<tr><td>517</td><td>0</td><td><span class=zero-hits>if deep_eq(a[i], v) == false then</span></td></tr>
<tr><td>518</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>519</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>520</td><td>0</td><td><span class=zero-hits>elseif a[i] ~= v then</span></td></tr>
<tr><td>521</td><td>0</td><td><span class=zero-hits>return false</span></td></tr>
<tr><td>522</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>523</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>524</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>525</td><td>0</td><td><span class=zero-hits>return true</span></td></tr>
<tr><td>526</td><td><span class='text-muted'>N/A</span></td><td>end</td>></tr>
<tr><td>527</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>528</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>529</td><td><span class='text-muted'>N/A</span></td><td>-- Return</td>></tr>
<tr><td>530</td><td><span class='text-muted'>N/A</span></td><td>--------------------------------------------------------------------------------</td>></tr>
<tr><td>531</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>532</td><td>1</td><td><span class=low-hits>return {</span></td></tr>
<tr><td>533</td><td>1</td><td><span class=low-hits>test = function()</span></td></tr>
<tr><td>534</td><td>1</td><td><span class=low-hits>return {</span></td></tr>
<tr><td>535</td><td>1</td><td><span class=low-hits>TEST = TEST,</span></td></tr>
<tr><td>536</td><td>1</td><td><span class=low-hits>CASE = CASE,</span></td></tr>
<tr><td>537</td><td>1</td><td><span class=low-hits>CHECK = CHECK,</span></td></tr>
<tr><td>538</td><td>1</td><td><span class=low-hits>FINISH = FINISH,</span></td></tr>
<tr><td>539</td><td>1</td><td><span class=low-hits>SKIP = SKIP,</span></td></tr>
<tr><td>540</td><td>1</td><td><span class=low-hits>FOCUS = FOCUS,</span></td></tr>
<tr><td>541</td><td>1</td><td><span class=low-hits>CHECK_EXPECT_ERR = CHECK_EXPECT_ERR,</span></td></tr>
<tr><td>542</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
<tr><td>543</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>544</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>545</td><td>1</td><td><span class=low-hits>benchmark = function()</span></td></tr>
<tr><td>546</td><td>1</td><td><span class=low-hits>return BENCH, START</span></td></tr>
<tr><td>547</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>548</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>549</td><td>1</td><td><span class=low-hits>disable_formatting = function()</span></td></tr>
<tr><td>550</td><td>0</td><td><span class=zero-hits>disable_ansi = true</span></td></tr>
<tr><td>551</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
<tr><td>552</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>553</td><td>1</td><td><span class=low-hits>print = print2,</span></td></tr>
<tr><td>554</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>555</td><td>1</td><td><span class=low-hits>seq = shallow_eq,</span></td></tr>
<tr><td>556</td><td>1</td><td><span class=low-hits>deq = deep_eq,</span></td></tr>
<tr><td>557</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
<tr><td>558</td><td>1</td><td><span class=low-hits>color = color,</span></td></tr>
<tr><td>559</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
</tbody></table></body></html>

2044
coverage/tests.luau.html vendored Normal file

File diff suppressed because it is too large Load diff

BIN
demo.rbxl

Binary file not shown.

View file

@ -1,48 +1,48 @@
local events = {} local events = {}
local function trackers_invoke(event, component, entity, ...) local function trackers_invoke(event, component, entity, ...)
local trackers = events[event][component] local trackers = events[event][component]
if not trackers then if not trackers then
return return
end end
for _, tracker in trackers do for _, tracker in trackers do
tracker(entity, data) tracker(entity, data)
end end
end end
local function trackers_init(event, component, fn) local function trackers_init(event, component, fn)
local ob = events[event] local ob = events[event]
return { return {
connect = function(component, fn) connect = function(component, fn)
local trackers = ob[component] local trackers = ob[component]
if not trackers then if not trackers then
trackers = {} trackers = {}
ob[component] = trackers ob[component] = trackers
end end
table.insert(trackers, fn) table.insert(trackers, fn)
end, end,
invoke = function(component, ...) invoke = function(component, ...)
trackers_invoke(event, component, ...) trackers_invoke(event, component, ...)
end end
} }
return function(component, fn) return function(component, fn)
local trackers = ob[component] local trackers = ob[component]
if not trackers then if not trackers then
trackers = {} trackers = {}
ob[component] = trackers ob[component] = trackers
end end
table.insert(trackers, fn) table.insert(trackers, fn)
end end
end end
local trackers = { local trackers = {
emplace = trackers_init("emplace"), emplace = trackers_init("emplace"),
add = trackers_init("added"), add = trackers_init("added"),
remove = trackers_init("removed") remove = trackers_init("removed")
} }
return trackers return trackers

View file

@ -1,67 +1,67 @@
--!optimize 2 --!optimize 2
--!native --!native
--!strict --!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard local __ = jecs.Wildcard
local std = ReplicatedStorage.std local std = ReplicatedStorage.std
local world = require(std.world) local world = require(std.world)
local Position = world:component() :: jecs.Entity<vector> local Position = world:component() :: jecs.Entity<vector>
local Previous = jecs.Rest local Previous = jecs.Rest
local pre = jecs.pair(Position, Previous) local pre = jecs.pair(Position, Previous)
local added = world local added = world
:query(Position) :query(Position)
:without(pre) :without(pre)
:cached() :cached()
local changed = world local changed = world
:query(Position, pre) :query(Position, pre)
:cached() :cached()
local removed = world local removed = world
:query(pre) :query(pre)
:without(Position) :without(Position)
:cached() :cached()
local children = {} local children = {}
for i = 1, 10 do for i = 1, 10 do
local e = world:entity() local e = world:entity()
world:set(e, Position, vector.create(i, i, i)) world:set(e, Position, vector.create(i, i, i))
table.insert(children, e) table.insert(children, e)
end end
local function flip() local function flip()
return math.random() > 0.5 return math.random() > 0.5
end end
local function system() local function system()
for i, child in children do for i, child in children do
world:set(child, Position, vector.create(i,i,i)) world:set(child, Position, vector.create(i,i,i))
end end
for e, p in added:iter() do for e, p in added:iter() do
world:set(e, pre, p) world:set(e, pre, p)
end end
for i, child in children do for i, child in children do
if flip() then if flip() then
world:set(child, Position, vector.create(i + 1, i + 1, i + 1)) world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
end end
end end
for e, new, old in changed:iter() do for e, new, old in changed:iter() do
if new ~= old then if new ~= old then
world:set(e, pre, new) world:set(e, pre, new)
end end
end end
for i, child in children do for i, child in children do
world:remove(child, Position) world:remove(child, Position)
end end
for e in removed:iter() do for e in removed:iter() do
world:remove(e, pre) world:remove(e, pre)
end end
end end
local scheduler = require(std.scheduler) local scheduler = require(std.scheduler)
scheduler.SYSTEM(system) scheduler.SYSTEM(system)
return 0 return 0

View file

@ -1,90 +1,90 @@
--!optimize 2 --!optimize 2
--!native --!native
--!strict --!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard local __ = jecs.Wildcard
local std = ReplicatedStorage.std local std = ReplicatedStorage.std
local world = require(std.world) local world = require(std.world)
local Position = world:component() :: jecs.Entity<vector> local Position = world:component() :: jecs.Entity<vector>
local Previous = jecs.Rest local Previous = jecs.Rest
local pre = jecs.pair(Position, Previous) local pre = jecs.pair(Position, Previous)
local added = world local added = world
:query(Position) :query(Position)
:without(pre) :without(pre)
:cached() :cached()
local changed = world local changed = world
:query(Position, pre) :query(Position, pre)
:cached() :cached()
local removed = world local removed = world
:query(pre) :query(pre)
:without(Position) :without(Position)
:cached() :cached()
local children = {} local children = {}
for i = 1, 10 do for i = 1, 10 do
local e = world:entity() local e = world:entity()
world:set(e, Position, vector.create(i, i, i)) world:set(e, Position, vector.create(i, i, i))
table.insert(children, e) table.insert(children, e)
end end
local function flip() local function flip()
return math.random() > 0.5 return math.random() > 0.5
end end
local entity_index = world.entity_index local entity_index = world.entity_index
local function copy(archetypes, id) local function copy(archetypes, id)
for _, archetype in archetypes do for _, archetype in archetypes do
local to = jecs.archetype_traverse_add(world, pre, archetype) local to = jecs.archetype_traverse_add(world, pre, archetype)
local columns = to.columns local columns = to.columns
local records = to.records local records = to.records
local old = columns[records[pre].column] local old = columns[records[pre].column]
local new = columns[records[id].column] local new = columns[records[id].column]
if to ~= archetype then if to ~= archetype then
for _, entity in archetype.entities do for _, entity in archetype.entities do
local r = jecs.entity_index_try_get_fast(entity_index, entity) local r = jecs.entity_index_try_get_fast(entity_index, entity)
jecs.entity_move(entity_index, entity, r, to) jecs.entity_move(entity_index, entity, r, to)
end end
end end
table.move(new, 1, #new, 1, old) table.move(new, 1, #new, 1, old)
end end
end end
local function system2() local function system2()
for i, child in children do for i, child in children do
world:set(child, Position, vector.create(i,i,i)) world:set(child, Position, vector.create(i,i,i))
end end
for e, p in added:iter() do for e, p in added:iter() do
end end
copy(added:archetypes(), Position) copy(added:archetypes(), Position)
for i, child in children do for i, child in children do
if flip() then if flip() then
world:set(child, Position, vector.create(i + 1, i + 1, i + 1)) world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
end end
end end
for e, new, old in changed:iter() do for e, new, old in changed:iter() do
if new ~= old then if new ~= old then
end end
end end
copy(changed:archetypes(), Position) copy(changed:archetypes(), Position)
for i, child in children do for i, child in children do
world:remove(child, Position) world:remove(child, Position)
end end
for e in removed:iter() do for e in removed:iter() do
world:remove(e, pre) world:remove(e, pre)
end end
end end
local scheduler = require(std.scheduler) local scheduler = require(std.scheduler)
scheduler.SYSTEM(system2) scheduler.SYSTEM(system2)
return 0 return 0

952
jecs.luau

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,186 +0,0 @@
site_name: Jecs
site_url: jecs.github.io/jecs
repo_name: ukendio/jecs
repo_url: https://github.com/ukendio/jecs
extra:
version:
provider: mike
theme:
name: material
custom_dir: docs/assets/overrides
logo: assets/logo
favicon: assets/logo-dark.svg
palette:
- media: "(prefers-color-scheme: dark)"
scheme: fusiondoc-dark
toggle:
icon: octicons/sun-24
title: Switch to light theme
- media: "(prefers-color-scheme: light)"
scheme: fusiondoc-light
toggle:
icon: octicons/moon-24
title: Switch to dark theme
font:
text: Plus Jakarta Sans
code: JetBrains Mono
features:
- navigation.tabs
- navigation.top
- navigation.sections
- navigation.instant
- navigation.indexes
- search.suggest
- search.highlight
icon:
repo: octicons/mark-github-16
extra_css:
- assets/theme/fusiondoc.css
- assets/theme/colours.css
- assets/theme/code.css
- assets/theme/paragraph.css
- assets/theme/page.css
- assets/theme/admonition.css
- assets/theme/404.css
- assets/theme/api-reference.css
- assets/theme/dev-tools.css
extra_javascript:
- assets/scripts/smooth-scroll.js
nav:
- Home: index.md
- Tutorials:
- Get Started: tutorials/index.md
- Installing Fusion: tutorials/get-started/installing-fusion.md
- Developer Tools: tutorials/get-started/developer-tools.md
- Getting Help: tutorials/get-started/getting-help.md
- Fundamentals:
- Scopes: tutorials/fundamentals/scopes.md
- Values: tutorials/fundamentals/values.md
- Observers: tutorials/fundamentals/observers.md
- Computeds: tutorials/fundamentals/computeds.md
- Tables:
- ForValues: tutorials/tables/forvalues.md
- ForKeys: tutorials/tables/forkeys.md
- ForPairs: tutorials/tables/forpairs.md
- Animation:
- Tweens: tutorials/animation/tweens.md
- Springs: tutorials/animation/springs.md
- Roblox:
- Hydration: tutorials/roblox/hydration.md
- New Instances: tutorials/roblox/new-instances.md
- Parenting: tutorials/roblox/parenting.md
- Events: tutorials/roblox/events.md
- Change Events: tutorials/roblox/change-events.md
- Outputs: tutorials/roblox/outputs.md
- References: tutorials/roblox/references.md
- Best Practices:
- Components: tutorials/best-practices/components.md
- Instance Handling: tutorials/best-practices/instance-handling.md
- Callbacks: tutorials/best-practices/callbacks.md
- State: tutorials/best-practices/state.md
- Sharing Values: tutorials/best-practices/sharing-values.md
- Error Safety: tutorials/best-practices/error-safety.md
- Optimisation: tutorials/best-practices/optimisation.md
- Examples:
- Home: examples/index.md
- Cookbook:
- examples/cookbook/index.md
- Player List: examples/cookbook/player-list.md
- Animated Computed: examples/cookbook/animated-computed.md
- Fetch Data From Server: examples/cookbook/fetch-data-from-server.md
- Light & Dark Theme: examples/cookbook/light-and-dark-theme.md
- Button Component: examples/cookbook/button-component.md
- Loading Spinner: examples/cookbook/loading-spinner.md
- Drag & Drop: examples/cookbook/drag-and-drop.md
- API Reference:
- api-reference/index.md
- General:
- Errors: api-reference/general/errors.md
- Types:
- Contextual: api-reference/general/types/contextual.md
- Version: api-reference/general/types/version.md
- Members:
- Contextual: api-reference/general/members/contextual.md
- Safe: api-reference/general/members/safe.md
- version: api-reference/general/members/version.md
- Memory:
- Types:
- Scope: api-reference/memory/types/scope.md
- ScopedObject: api-reference/memory/types/scopedobject.md
- Task: api-reference/memory/types/task.md
- Members:
- deriveScope: api-reference/memory/members/derivescope.md
- doCleanup: api-reference/memory/members/docleanup.md
- scoped: api-reference/memory/members/scoped.md
- State:
- Types:
- UsedAs: api-reference/state/types/usedas.md
- Computed: api-reference/state/types/computed.md
- Dependency: api-reference/state/types/dependency.md
- Dependent: api-reference/state/types/dependent.md
- For: api-reference/state/types/for.md
- Observer: api-reference/state/types/observer.md
- StateObject: api-reference/state/types/stateobject.md
- Use: api-reference/state/types/use.md
- Value: api-reference/state/types/value.md
- Members:
- Computed: api-reference/state/members/computed.md
- ForKeys: api-reference/state/members/forkeys.md
- ForPairs: api-reference/state/members/forpairs.md
- ForValues: api-reference/state/members/forvalues.md
- Observer: api-reference/state/members/observer.md
- peek: api-reference/state/members/peek.md
- Value: api-reference/state/members/value.md
- Roblox:
- Types:
- Child: api-reference/roblox/types/child.md
- PropertyTable: api-reference/roblox/types/propertytable.md
- SpecialKey: api-reference/roblox/types/specialkey.md
- Members:
- Attribute: api-reference/roblox/members/attribute.md
- AttributeChange: api-reference/roblox/members/attributechange.md
- AttributeOut: api-reference/roblox/members/attributeout.md
- Children: api-reference/roblox/members/children.md
- Hydrate: api-reference/roblox/members/hydrate.md
- New: api-reference/roblox/members/new.md
- OnChange: api-reference/roblox/members/onchange.md
- OnEvent: api-reference/roblox/members/onevent.md
- Out: api-reference/roblox/members/out.md
- Ref: api-reference/roblox/members/ref.md
- Animation:
- Types:
- Animatable: api-reference/animation/types/animatable.md
- Spring: api-reference/animation/types/spring.md
- Tween: api-reference/animation/types/tween.md
- Members:
- Tween: api-reference/animation/members/tween.md
- Spring: api-reference/animation/members/spring.md
- Extras:
- Home: extras/index.md
- Backgrounds: extras/backgrounds.md
- Brand Guidelines: extras/brand-guidelines.md
markdown_extensions:
- admonition
- attr_list
- meta
- md_in_html
- pymdownx.superfences
- pymdownx.betterem
- pymdownx.details
- pymdownx.tabbed:
alternate_style: true
- pymdownx.inlinehilite
- toc:
permalink: true
- pymdownx.highlight:
guess_lang: false
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg

4464
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,5 +2,4 @@
wally = "upliftgames/wally@0.3.2" wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.4" rojo = "rojo-rbx/rojo@7.4.4"
stylua = "johnnymorganz/stylua@2.0.1" stylua = "johnnymorganz/stylua@2.0.1"
selene = "kampfkarren/selene@0.27.1"
Blink = "1Axen/Blink@0.14.1" Blink = "1Axen/Blink@0.14.1"

View file

@ -1,4 +0,0 @@
std = "roblox"
[lints]
global_usage = "allow"

View file

@ -1,25 +1,25 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local pair = jecs.pair local pair = jecs.pair
local ChildOf = jecs.ChildOf local ChildOf = jecs.ChildOf
local lifetime_tracker_add = require("@tools/lifetime_tracker") local lifetime_tracker_add = require("@tools/lifetime_tracker")
local pe = require("@tools/entity_visualiser").prettify local pe = require("@tools/entity_visualiser").prettify
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false}) local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
local FriendsWith = world:component() local FriendsWith = world:component()
local _1 = world:print_snapshot() world:print_snapshot()
local e1 = world:entity() local e1 = world:entity()
local e2 = world:entity() local e2 = world:entity()
world:delete(e2) world:delete(e2)
local _2 = world:print_snapshot() world:print_snapshot()
local e3 = world:entity() local e3 = world:entity()
world:add(e3, pair(ChildOf, e1)) world:add(e3, pair(ChildOf, e1))
local e4 = world:entity() local e4 = world:entity()
world:add(e4, pair(FriendsWith, e3)) world:add(e4, pair(FriendsWith, e3))
local _3 = world:print_snapshot() world:print_snapshot()
world:delete(e1) world:delete(e1)
world:delete(e3) world:delete(e3)
local _4 = world:print_snapshot() world:print_snapshot()
world:print_entity_index() world:print_entity_index()
world:entity() world:entity()
world:entity() world:entity()
local _5 = world:print_snapshot() world:print_snapshot()

View file

@ -1,158 +1,158 @@
local c = { local c = {
white_underline = function(s: any) white_underline = function(s: any)
return `\27[1;4m{s}\27[0m` return `\27[1;4m{s}\27[0m`
end, end,
white = function(s: any) white = function(s: any)
return `\27[37;1m{s}\27[0m` return `\27[37;1m{s}\27[0m`
end, end,
green = function(s: any) green = function(s: any)
return `\27[32;1m{s}\27[0m` return `\27[32;1m{s}\27[0m`
end, end,
red = function(s: any) red = function(s: any)
return `\27[31;1m{s}\27[0m` return `\27[31;1m{s}\27[0m`
end, end,
yellow = function(s: any) yellow = function(s: any)
return `\27[33;1m{s}\27[0m` return `\27[33;1m{s}\27[0m`
end, end,
red_highlight = function(s: any) red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m` return `\27[41;1;30m{s}\27[0m`
end, end,
green_highlight = function(s: any) green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m` return `\27[42;1;30m{s}\27[0m`
end, end,
gray = function(s: any) gray = function(s: any)
return `\27[30;1m{s}\27[0m` return `\27[30;1m{s}\27[0m`
end, end,
} }
local ECS_PAIR_FLAG = 0x8 local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10 local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_GENERATION_MASK = bit32.lshift(1, 16)
type i53 = number type i53 = number
type i24 = number type i24 = number
local function ECS_ENTITY_T_LO(e: i53): i24 local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end end
local function ECS_GENERATION(e: i53): i24 local function ECS_GENERATION(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
end end
local ECS_ID = ECS_ENTITY_T_LO local ECS_ID = ECS_ENTITY_T_LO
local function ECS_COMBINE(source: number, target: number): i53 local function ECS_COMBINE(source: number, target: number): i53
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
end end
local function ECS_GENERATION_INC(e: i53) local function ECS_GENERATION_INC(e: i53)
if e > ECS_ENTITY_MASK then if e > ECS_ENTITY_MASK then
local flags = e // ECS_ID_FLAGS_MASK local flags = e // ECS_ID_FLAGS_MASK
local id = flags // ECS_ENTITY_MASK local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK local generation = flags % ECS_GENERATION_MASK
local next_gen = generation + 1 local next_gen = generation + 1
if next_gen > ECS_GENERATION_MASK then if next_gen > ECS_GENERATION_MASK then
return id return id
end end
return ECS_COMBINE(id, next_gen) + flags return ECS_COMBINE(id, next_gen) + flags
end end
return ECS_COMBINE(e, 1) return ECS_COMBINE(e, 1)
end end
local function bl() local function bl()
print("") print("")
end end
local function pe(e) local function pe(e)
local gen = ECS_GENERATION(e) local gen = ECS_GENERATION(e)
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`) return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
end end
local function dprint(tbl: { [number]: number }) local function dprint(tbl: { [number]: number })
bl() bl()
print("--------") print("--------")
for i, e in tbl do for i, e in tbl do
print("| "..pe(e).." |") print("| "..pe(e).." |")
print("--------") print("--------")
end end
bl() bl()
end end
local max_id = 0 local max_id = 0
local alive_count = 0 local alive_count = 0
local dense = {} local dense = {}
local sparse = {} local sparse = {}
local function alloc() local function alloc()
if alive_count ~= #dense then if alive_count ~= #dense then
alive_count += 1 alive_count += 1
print("*recycled", pe(dense[alive_count])) print("*recycled", pe(dense[alive_count]))
return dense[alive_count] return dense[alive_count]
end end
max_id += 1 max_id += 1
local id = max_id local id = max_id
alive_count += 1 alive_count += 1
dense[alive_count] = id dense[alive_count] = id
sparse[id] = { sparse[id] = {
dense = alive_count dense = alive_count
} }
print("*allocated", pe(id)) print("*allocated", pe(id))
return id return id
end end
local function remove(entity) local function remove(entity)
local id = ECS_ID(entity) local id = ECS_ID(entity)
local r = sparse[id] local r = sparse[id]
local index_of_deleted_entity = r.dense local index_of_deleted_entity = r.dense
local last_entity_alive_at_index = alive_count -- last entity alive local last_entity_alive_at_index = alive_count -- last entity alive
alive_count -= 1 alive_count -= 1
local last_alive_entity = dense[last_entity_alive_at_index] local last_alive_entity = dense[last_entity_alive_at_index]
local r_swap = sparse[ECS_ID(last_alive_entity)] local r_swap = sparse[ECS_ID(last_alive_entity)]
r_swap.dense = r.dense r_swap.dense = r.dense
r.dense = last_entity_alive_at_index r.dense = last_entity_alive_at_index
dense[index_of_deleted_entity] = last_alive_entity dense[index_of_deleted_entity] = last_alive_entity
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
print("*dellocated", pe(id)) print("*dellocated", pe(id))
end end
local function alive(e) local function alive(e)
local r = sparse[ECS_ID(e)] local r = sparse[ECS_ID(e)]
return dense[r.dense] == e return dense[r.dense] == e
end end
local function pa(e) local function pa(e)
print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`) print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`)
end end
local tprint = require("@testkit").print local tprint = require("@testkit").print
local e1v0 = alloc() local e1v0 = alloc()
local e2v0 = alloc() local e2v0 = alloc()
local e3v0 = alloc() local e3v0 = alloc()
local e4v0 = alloc() local e4v0 = alloc()
local e5v0 = alloc() local e5v0 = alloc()
pa(e1v0) pa(e1v0)
pa(e4v0) pa(e4v0)
remove(e5v0) remove(e5v0)
pa(e5v0) pa(e5v0)
local e5v1 = alloc() local e5v1 = alloc()
pa(e5v0) pa(e5v0)
pa(e5v1) pa(e5v1)
pa(e2v0) pa(e2v0)
print(ECS_ID(e2v0)) print(ECS_ID(e2v0))
dprint(dense) dprint(dense)
remove(e2v0) remove(e2v0)
dprint(dense) dprint(dense)

View file

@ -1,122 +1,122 @@
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
_G.__JECS_HI_COMPONENT_ID = 300 _G.__JECS_HI_COMPONENT_ID = 300
local ecs = require(ReplicatedStorage.ecs) local ecs = require(ReplicatedStorage.ecs)
-- 500 entities -- 500 entities
-- 2-30 components on each entity -- 2-30 components on each entity
-- 300 unique components -- 300 unique components
-- 200 systems -- 200 systems
-- 1-10 components to query per system -- 1-10 components to query per system
local startTime = os.clock() local startTime = os.clock()
local world = ecs.World.new() local world = ecs.World.new()
local components = {} local components = {}
for i = 1, 300 do -- 300 components for i = 1, 300 do -- 300 components
components[i] = world:component() components[i] = world:component()
end end
local archetypes = {} local archetypes = {}
for i = 1, 50 do -- 50 archetypes for i = 1, 50 do -- 50 archetypes
local archetype = {} local archetype = {}
for _ = 1, math.random(2, 30) do for _ = 1, math.random(2, 30) do
local componentId = math.random(1, #components) local componentId = math.random(1, #components)
table.insert(archetype, components[componentId]) table.insert(archetype, components[componentId])
end end
archetypes[i] = archetype archetypes[i] = archetype
end end
for _ = 1, 1000 do -- 1000 entities in the world for _ = 1, 1000 do -- 1000 entities in the world
local componentsToAdd = {} local componentsToAdd = {}
local archetypeId = math.random(1, #archetypes) local archetypeId = math.random(1, #archetypes)
local e = world:entity() local e = world:entity()
for _, component in ipairs(archetypes[archetypeId]) do for _, component in ipairs(archetypes[archetypeId]) do
world:set(e, component, { world:set(e, component, {
DummyData = math.random(1, 5000), DummyData = math.random(1, 5000),
}) })
end end
end end
local function values(t) local function values(t)
local array = {} local array = {}
for _, v in t do for _, v in t do
table.insert(array, v) table.insert(array, v)
end end
return array return array
end end
local contiguousComponents = values(components) local contiguousComponents = values(components)
local systemComponentsToQuery = {} local systemComponentsToQuery = {}
for _ = 1, 200 do -- 200 systems for _ = 1, 200 do -- 200 systems
local numComponentsToQuery = math.random(1, 10) local numComponentsToQuery = math.random(1, 10)
local componentsToQuery = {} local componentsToQuery = {}
for _ = 1, numComponentsToQuery do for _ = 1, numComponentsToQuery do
table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)]) table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)])
end end
table.insert(systemComponentsToQuery, componentsToQuery) table.insert(systemComponentsToQuery, componentsToQuery)
end end
local worldCreateTime = os.clock() - startTime local worldCreateTime = os.clock() - startTime
local results = {} local results = {}
startTime = os.clock() startTime = os.clock()
RunService.Heartbeat:Connect(function() RunService.Heartbeat:Connect(function()
local added = 0 local added = 0
local systemStartTime = os.clock() local systemStartTime = os.clock()
debug.profilebegin("systems") debug.profilebegin("systems")
for _, componentsToQuery in ipairs(systemComponentsToQuery) do for _, componentsToQuery in ipairs(systemComponentsToQuery) do
debug.profilebegin("system") debug.profilebegin("system")
for entityId, firstComponent in world:query(unpack(componentsToQuery)) do for entityId, firstComponent in world:query(unpack(componentsToQuery)) do
world:set( world:set(
entityId, entityId,
{ {
DummyData = firstComponent.DummyData + 1, DummyData = firstComponent.DummyData + 1,
} }
) )
added += 1 added += 1
end end
debug.profileend() debug.profileend()
end end
debug.profileend() debug.profileend()
if os.clock() - startTime < 4 then if os.clock() - startTime < 4 then
-- discard first 4 seconds -- discard first 4 seconds
return return
end end
if results == nil then if results == nil then
return return
elseif #results < 1000 then elseif #results < 1000 then
table.insert(results, os.clock() - systemStartTime) table.insert(results, os.clock() - systemStartTime)
else else
print("added", added) print("added", added)
print("World created in", worldCreateTime * 1000, "ms") print("World created in", worldCreateTime * 1000, "ms")
local sum = 0 local sum = 0
for _, result in ipairs(results) do for _, result in ipairs(results) do
sum += result sum += result
end end
print(("Average frame time: %fms"):format((sum / #results) * 1000)) print(("Average frame time: %fms"):format((sum / #results) * 1000))
results = nil results = nil
local n = #world.archetypes local n = #world.archetypes
print( print(
("X entities\n%d components\n%d systems\n%d archetypes"):format( ("X entities\n%d components\n%d systems\n%d archetypes"):format(
#components, #components,
#systemComponentsToQuery, #systemComponentsToQuery,
n n
) )
) )
end end
end) end)

View file

@ -117,6 +117,75 @@ local function name(world, e)
return world:get(e, jecs.Name) return world:get(e, jecs.Name)
end end
TEST("#adding a recycled target", function()
local world = world_new()
local R = world:component()
local e = world:entity()
local T = world:entity()
world:add(e, pair(R, T))
world:delete(T)
CHECK(not world:has(e, pair(R, T)))
local T2 = world:entity()
world:add(e, pair(R, T2))
CHECK(world:target(e, R) ~= T)
CHECK(world:target(e, R) ~= 0)
end)
TEST("#repro2", function()
local world = world_new()
local Lifetime = world:component() :: jecs.Id<number>
local Particle = world:entity()
local Beam = world:entity()
local entity = world:entity()
world:set(entity, pair(Lifetime, Particle), 1)
world:set(entity, pair(Lifetime, Beam), 2)
world:set(entity, pair(4, 5), 6) -- noise
local entity_visualizer = require("@tools/entity_visualiser")
-- entity_visualizer.components(world, entity)
for e in world:each(pair(Lifetime, __)) do
local i = 0
local nth = world:target(e, Lifetime, i)
while nth do
-- entity_visualizer.components(world, e)
local data = world:get(e, pair(Lifetime, nth))
data -= 1
if data <= 0 then
world:remove(e, pair(Lifetime, nth))
else
world:set(e, pair(Lifetime, nth), data)
end
i += 1
nth = world:target(e, Lifetime, i)
end
end
CHECK(not world:has(entity, pair(Lifetime, Particle)))
CHECK(world:get(entity, pair(Lifetime, Beam)) == 1)
end)
local lifetime_tracker_add = require("@tools/lifetime_tracker")
TEST("another", function()
local world = world_new()
-- world = lifetime_tracker_add(world, {padding_enabled=false})
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
world:delete(e2)
local e2_e3 = pair(e2, e3)
CHECK(jecs.pair_first(world, e2_e3) == 0)
CHECK(jecs.pair_second(world, e2_e3) == e3)
CHECK_EXPECT_ERR(function()
world:add(e1, pair(e2, e3))
end)
end)
TEST("#repro", function() TEST("#repro", function()
local world = world_new() local world = world_new()
@ -186,9 +255,9 @@ end)
TEST("world:cleanup()", function() TEST("world:cleanup()", function()
local world = world_new() local world = world_new()
local A = world:component() local A = world:component() :: jecs.Id<boolean>
local B = world:component() local B = world:component() :: jecs.Id<boolean>
local C = world:component() local C = world:component() :: jecs.Id<boolean>
local e1 = world:entity() local e1 = world:entity()
local e2 = world:entity() local e2 = world:entity()
@ -299,7 +368,7 @@ TEST("world:entity()", function()
do CASE "Recycling max generation" do CASE "Recycling max generation"
local world = world_new() local world = world_new()
local pin = jecs.Rest::number + 1 local pin = (jecs.Rest :: any) :: number + 1
for i = 1, 2^16-1 do for i = 1, 2^16-1 do
local e = world:entity() local e = world:entity()
world:delete(e) world:delete(e)
@ -513,21 +582,22 @@ TEST("world:query()", function()
do CASE "pairs" do CASE "pairs"
local world = jecs.World.new() local world = jecs.World.new()
local C1 = world:component() local C1 = world:component() :: jecs.Id<boolean>
local C2 = world:component() local C2 = world:component() :: jecs.Id<boolean>
local T1 = world:entity() local T1 = world:entity()
local T2 = world:entity() local T2 = world:entity()
local e = world:entity() local e = world:entity()
world:set(e, pair(C1, C2), true) local C1_C2 = pair(C1, C2)
world:set(e, C1_C2, true)
world:set(e, pair(C1, T1), true) world:set(e, pair(C1, T1), true)
world:set(e, pair(T1, C1), true) world:set(e, pair(T1, C1), true)
CHECK_EXPECT_ERR(function() CHECK_EXPECT_ERR(function()
world:set(e, pair(T1, T2), true :: any) world:set(e, pair(T1, T2), true :: any)
end) end)
for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) :: any do for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)):iter() do
CHECK(a == true) CHECK(a == true)
CHECK(b == true) CHECK(b == true)
CHECK(c == true) CHECK(c == true)
@ -823,8 +893,9 @@ TEST("world:query()", function()
local bob = world:entity() local bob = world:entity()
world:delete(Apples) world:delete(Apples)
CHECK_EXPECT_ERR(function()
world:set(bob, pair(Eats, Apples), "bob eats apples") world:set(bob, pair(Eats, Apples), "bob eats apples")
end)
end end
do do
@ -871,10 +942,8 @@ TEST("world:query()", function()
CHECK(count == 2) CHECK(count == 2)
end end
do do CASE("iterator invalidation")
CASE("iterator invalidation") do CASE("adding")
do
CASE("adding")
SKIP() SKIP()
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
@ -896,8 +965,7 @@ TEST("world:query()", function()
CHECK(count == 2) CHECK(count == 2)
end end
do do CASE("spawning")
CASE("spawning")
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -918,8 +986,7 @@ TEST("world:query()", function()
end end
end end
do do CASE("should not find any entities")
CASE("should not find any entities")
local world = jecs.World.new() local world = jecs.World.new()
local Hello = world:component() local Hello = world:component()
@ -937,19 +1004,16 @@ TEST("world:query()", function()
CHECK(withoutCount == 0) CHECK(withoutCount == 0)
end end
do do CASE("without")
CASE("without") -- REGRESSION TEST
do local world = jecs.World.new()
-- REGRESSION TEST local _1, _2, _3 = world:component(), world:component(), world:component()
local world = jecs.World.new()
local _1, _2, _3 = world:component(), world:component(), world:component()
local counter = 0 local counter = 0
for e, a, b in world:query(_1, _2):without(_3) :: any do for e, a, b in world:query(_1, _2):without(_3) :: any do
counter += 1 counter += 1
end
CHECK(counter == 0)
end end
CHECK(counter == 0)
end end
end) end)
@ -1019,73 +1083,106 @@ TEST("world:children", function()
end) end)
TEST("world:clear()", function() TEST("world:clear()", function()
do do CASE("should remove its components")
CASE("should remove its components")
local world = jecs.World.new() :: World local world = jecs.World.new() :: World
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity() local e = world:entity()
local e1 = world:entity()
local e2 = world:entity()
world:set(e, A, true) world:set(e, A, true)
world:set(e, B, true) world:set(e, B, true)
world:set(e1, A, true)
world:set(e1, B, true)
CHECK(world:get(e, A)) CHECK(world:get(e, A))
CHECK(world:get(e, B)) CHECK(world:get(e, B))
world:clear(e) world:clear(A)
CHECK(world:get(e, A) == nil) CHECK(world:get(e, A) == nil)
CHECK(world:get(e, B) == nil) CHECK(world:get(e, B))
CHECK(world:get(e1, A) == nil)
CHECK(world:get(e1, B))
end end
do do CASE("remove cleared ID from entities")
CASE("should move last record")
local world = world_new() local world = world_new()
local A = world:component() local A = world:component()
local B = world:component()
local C = world:component()
local e = world:entity() do
local e1 = world:entity() local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:add(e, A) world:set(id1, A, true)
world:add(e1, A)
local archetype = world.archetype_index["1"] world:set(id2, A, true)
local archetype_entities = archetype.entities world:set(id2, B, true)
local _e = e :: number world:set(id3, A, true)
local _e1 = e1 :: number world:set(id3, B, true)
world:set(id3, C, true)
CHECK(archetype_entities[1] == _e) world:clear(A)
CHECK(archetype_entities[2] == _e1)
local e_record: jecs.Record = entity_index_try_get_any( CHECK(not world:has(id1, A))
world.entity_index, e) :: any CHECK(not world:has(id2, A))
local e1_record: jecs.Record = entity_index_try_get_any( CHECK(not world:has(id3, A))
world.entity_index, e1) :: any
CHECK(e_record.archetype == archetype)
CHECK(e1_record.archetype == archetype)
CHECK(e1_record.row == 2)
world:clear(e) CHECK(world:has(id2, B))
CHECK(world:has(id3, B, C))
CHECK((e_record.archetype :: jecs.Archetype?) == nil) world:clear(C)
CHECK((e_record.row :: number?) == nil)
CHECK(e1_record.archetype == archetype)
CHECK(e1_record.row == 1)
CHECK(archetype_entities[1] == _e1) CHECK(world:has(id2, B))
CHECK(archetype_entities[2] == nil) CHECK(world:has(id3, B))
CHECK(world:contains(A))
CHECK(world:contains(C))
CHECK(world:has(A, jecs.Component))
CHECK(world:has(B, jecs.Component))
end
do
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
local tgt = world:entity()
world:add(id1, pair(A, tgt))
world:add(id1, pair(B, tgt))
world:add(id1, pair(C, tgt))
world:add(id2, pair(A, tgt))
world:add(id2, pair(B, tgt))
world:add(id2, pair(C, tgt))
world:add(id3, pair(A, tgt))
world:add(id3, pair(B, tgt))
world:add(id3, pair(C, tgt))
world:clear(B)
CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
end
CHECK(world:contains(e) == true)
CHECK(world:has(e, A) == false)
CHECK(world:contains(e1) == true)
CHECK(world:has(e1, A) == true)
end end
end) end)
TEST("world:has()", function() TEST("world:has()", function()
do do CASE("should find Tag on entity")
CASE("should find Tag on entity")
local world = jecs.World.new() local world = jecs.World.new()
local Tag = world:entity() local Tag = world:entity()
@ -1096,8 +1193,7 @@ TEST("world:has()", function()
CHECK(world:has(e, Tag)) CHECK(world:has(e, Tag))
end end
do do CASE("should return false when missing one tag")
CASE("should return false when missing one tag")
local world = jecs.World.new() local world = jecs.World.new()
local A = world:entity() local A = world:entity()
@ -1115,8 +1211,7 @@ TEST("world:has()", function()
end) end)
TEST("world:component()", function() TEST("world:component()", function()
do do CASE("only components should have EcsComponent trait")
CASE("only components should have EcsComponent trait")
local world = jecs.World.new() :: World local world = jecs.World.new() :: World
local A = world:component() local A = world:component()
local e = world:entity() local e = world:entity()
@ -1196,8 +1291,7 @@ TEST("world:delete", function()
CHECK(not world:contains(tag)) CHECK(not world:contains(tag))
CHECK(not world:has(entity, tag)) -- => true CHECK(not world:has(entity, tag)) -- => true
end end
do do CASE("should allow deleting components")
CASE("should allow deleting components")
local world = jecs.World.new() local world = jecs.World.new()
local Health = world:component() local Health = world:component()
@ -1219,8 +1313,7 @@ TEST("world:delete", function()
CHECK(world:get(id1, Health) == 50) CHECK(world:get(id1, Health) == 50)
end end
do do CASE("delete entities using another Entity as component with Delete cleanup action")
CASE("delete entities using another Entity as component with Delete cleanup action")
local world = jecs.World.new() local world = jecs.World.new()
local Health = world:entity() local Health = world:entity()
@ -1253,8 +1346,8 @@ TEST("world:delete", function()
CHECK(not world:has(id1, Health)) CHECK(not world:has(id1, Health))
end end
do
CASE("delete children") do CASE("delete children")
local world = jecs.World.new() local world = jecs.World.new()
local Health = world:component() local Health = world:component()
@ -1307,6 +1400,100 @@ TEST("world:delete", function()
end end
end end
do CASE("remove deleted ID from entities")
local world = world_new()
do
local A = world:component()
local B = world:component()
local C = world:component()
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id1, A, true)
world:set(id2, A, true)
world:set(id2, B, true)
world:set(id3, A, true)
world:set(id3, B, true)
world:set(id3, C, true)
world:delete(A)
CHECK(not world:has(id1, A))
CHECK(not world:has(id2, A))
CHECK(not world:has(id3, A))
CHECK(world:has(id2, B))
CHECK(world:has(id3, B, C))
world:delete(C)
CHECK(world:has(id2, B))
CHECK(world:has(id3, B))
CHECK(not world:contains(A))
CHECK(not world:contains(C))
end
do
local A = world:component()
world:add(A, pair(jecs.OnDeleteTarget, jecs.Delete))
local B = world:component()
local C = world:component()
world:add(C, pair(jecs.OnDeleteTarget, jecs.Delete))
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id1, C, true)
world:set(id2, pair(A, id1), true)
world:set(id2, B, true)
world:set(id3, B, true)
world:set(id3, pair(C, id2), true)
world:delete(id1)
CHECK(not world:contains(id1))
CHECK(not world:contains(id2))
CHECK(not world:contains(id3))
end
do
local A = world:component()
local B = world:component()
local C = world:component()
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id2, A, true)
world:set(id2, pair(B, id1), true)
world:set(id3, A, true)
world:set(id3, pair(B, id1), true)
world:set(id3, C, true)
world:delete(id1)
CHECK(not world:contains(id1))
CHECK(world:contains(id2))
CHECK(world:contains(id3))
CHECK(world:has(id2, A))
CHECK(world:has(id3, A, C))
CHECK(not world:target(id2, B))
CHECK(not world:target(id3, B))
end
end
do do
CASE("fast delete") CASE("fast delete")
local world = jecs.World.new() local world = jecs.World.new()
@ -1394,24 +1581,10 @@ TEST("world:target", function()
CHECK(world:target(e, C, 0) == D) CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil) CHECK(world:target(e, C, 1) == nil)
-- for id in archetype.records do
-- local f = world:get(ecs_pair_first(world, id), jecs.Name)
-- local s = world:get(ecs_pair_second(world, id), jecs.Name)
-- print(`({f}, {s})`)
-- end
--
CHECK(archetype.records[pair(A, B)] == 1) CHECK(archetype.records[pair(A, B)] == 1)
CHECK(archetype.records[pair(A, C)] == 2) CHECK(archetype.records[pair(A, C)] == 2)
CHECK(archetype.records[pair(A, D)] == 3) CHECK(archetype.records[pair(A, D)] == 3)
CHECK(archetype.records[pair(A, E)] == 4) CHECK(archetype.records[pair(A, E)] == 4)
-- print("(A, B)", archetype.records[pair(A, B)])
-- print("(A, C)", archetype.records[pair(A, C)])
-- print("(A, D)", archetype.records[pair(A, D)])
-- print("(A, E)", archetype.records[pair(A, E)])
-- print(pair(A, D), pair(B, C))
-- print("(B, C)", archetype.records[pair(B, C)])
CHECK(world:target(e, C, 0) == D) CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil) CHECK(world:target(e, C, 1) == nil)

View file

@ -1,24 +0,0 @@
declare function afterAll(callback: () -> ()): ()
declare function afterEach(callback: () -> ()): ()
declare function beforeAll(callback: () -> ()): ()
declare function beforeEach(callback: () -> ()): ()
declare function describe(phrase: string, callback: () -> ()): ()
declare function describeFOCUS(phrase: string, callback: () -> ()): ()
declare function fdescribe(phrase: string, callback: () -> ()): ()
declare function describeSKIP(phrase: string, callback: () -> ()): ()
declare function xdescribe(phrase: string, callback: () -> ()): ()
declare function expect(value: any): any
declare function FIXME(optionalMessage: string?): ()
declare function FOCUS(): ()
declare function SKIP(): ()
declare function it(phrase: string, callback: () -> ()): ()
declare function itFOCUS(phrase: string, callback: () -> ()): ()
declare function fit(phrase: string, callback: () -> ()): ()
declare function itSKIP(phrase: string, callback: () -> ()): ()
declare function xit(phrase: string, callback: () -> ()): ()
declare function itFIXME(phrase: string, callback: () -> ()): ()

View file

@ -1,33 +1,33 @@
return { return {
white_underline = function(s: any) white_underline = function(s: any)
return `\27[1;4m{s}\27[0m` return `\27[1;4m{s}\27[0m`
end, end,
white = function(s: any) white = function(s: any)
return `\27[37;1m{s}\27[0m` return `\27[37;1m{s}\27[0m`
end, end,
green = function(s: any) green = function(s: any)
return `\27[32;1m{s}\27[0m` return `\27[32;1m{s}\27[0m`
end, end,
red = function(s: any) red = function(s: any)
return `\27[31;1m{s}\27[0m` return `\27[31;1m{s}\27[0m`
end, end,
yellow = function(s: any) yellow = function(s: any)
return `\27[33;1m{s}\27[0m` return `\27[33;1m{s}\27[0m`
end, end,
red_highlight = function(s: any) red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m` return `\27[41;1;30m{s}\27[0m`
end, end,
green_highlight = function(s: any) green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m` return `\27[42;1;30m{s}\27[0m`
end, end,
gray = function(s: any) gray = function(s: any)
return `\27[30;1m{s}\27[0m` return `\27[30;1m{s}\27[0m`
end, end,
} }

View file

@ -1,43 +1,43 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local ECS_GENERATION = jecs.ECS_GENERATION local ECS_GENERATION = jecs.ECS_GENERATION
local ECS_ID = jecs.ECS_ID local ECS_ID = jecs.ECS_ID
local ansi = require("@tools/ansi") local ansi = require("@tools/ansi")
local function pe(e: any) local function pe(e: any)
local gen = ECS_GENERATION(e) local gen = ECS_GENERATION(e)
return ansi.green(`e{ECS_ID(e)}`)..ansi.yellow(`v{gen}`) return ansi.green(`e{ECS_ID(e)}`) .. ansi.yellow(`v{gen}`)
end end
local function name(world: jecs.World, id: any) local function name(world: jecs.World, id: any)
return world:get(id, jecs.Name) or `${id}` return world:get(id, jecs.Name) or `${id}`
end end
local function components(world: jecs.World, entity: any) local function components(world: jecs.World, entity: any)
local r = jecs.entity_index_try_get(world.entity_index, entity) local r = jecs.entity_index_try_get(world.entity_index, entity)
if not r then if not r then
return false return false
end end
local archetype = r.archetype local archetype = r.archetype
local row = r.row local row = r.row
print(`Entity {pe(entity)}`) print(`Entity {pe(entity)}`)
print("-----------------------------------------------------") print("-----------------------------------------------------")
for i, column in archetype.columns do for i, column in archetype.columns do
local component = archetype.types[i] local component = archetype.types[i]
local n local n
if jecs.IS_PAIR(component) then if jecs.IS_PAIR(component) then
n = `({name(world, jecs.pair_first(world, component))}, {name(world, jecs.pair_second(world, component))})` n = `({name(world, jecs.pair_first(world, component))}, {name(world, jecs.pair_second(world, component))})`
else else
n = name(world, component) n = name(world, component)
end end
local data = column[row] or "TAG" local data = column[row] or "TAG"
print(`| {n} | {data} |`) print(`| {n} | {data} |`)
end end
print("-----------------------------------------------------") print("-----------------------------------------------------")
return true return true
end end
return { return {
components = components, components = components,
prettify = pe, prettify = pe,
} }

View file

@ -1,215 +1,216 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local ECS_GENERATION = jecs.ECS_GENERATION local ECS_GENERATION = jecs.ECS_GENERATION
local ECS_ID = jecs.ECS_ID local ECS_ID = jecs.ECS_ID
local __ = jecs.Wildcard local __ = jecs.Wildcard
local pair = jecs.pair local pair = jecs.pair
local prettify = require("@tools/entity_visualiser").prettify local prettify = require("@tools/entity_visualiser").prettify
local pe = prettify local pe = prettify
local ansi = require("@tools/ansi") local ansi = require("@tools/ansi")
function print_centered_entity(entity, width: number) function print_centered_entity(entity, width: number)
local entity_str = tostring(entity) local entity_str = tostring(entity)
local entity_length = #entity_str local entity_length = #entity_str
local padding_total = width - 2 - entity_length local padding_total = width - 2 - entity_length
local padding_left = math.floor(padding_total / 2) local padding_left = math.floor(padding_total / 2)
local padding_right = padding_total - padding_left local padding_right = padding_total - padding_left
local centered_str = string.rep(" ", padding_left) .. entity_str .. string.rep(" ", padding_right) local centered_str = string.rep(" ", padding_left) .. entity_str .. string.rep(" ", padding_right)
print("|" .. centered_str .. "|") print("|" .. centered_str .. "|")
end end
local function name(world, e) local function name(world, e)
return world:get(world, e, jecs.Name) or pe(e) return world:get(world, e, jecs.Name) or pe(e)
end end
local padding_enabled = false local padding_enabled = false
local function pad() local function pad()
if padding_enabled then if padding_enabled then
print("") print("")
end end
end end
local function lifetime_tracker_add(world: jecs.World, opt) local function lifetime_tracker_add(world: jecs.World, opt)
local entity_index = world.entity_index local entity_index = world.entity_index
local dense_array = entity_index.dense_array local dense_array = entity_index.dense_array
local component_index = world.component_index local component_index = world.component_index
local ENTITY_RANGE = (jecs.Rest :: any) + 1 local ENTITY_RANGE = (jecs.Rest :: any) + 1
local w = setmetatable({}, { __index = world }) local w = setmetatable({}, { __index = world })
padding_enabled = opt.padding_enabled padding_enabled = opt.padding_enabled
local world_entity = world.entity local world_entity = world.entity
w.entity = function(self, entity) w.entity = function(self, entity)
if entity then if entity then
return world_entity(world, entity) return world_entity(world, entity)
end end
local will_recycle = entity_index.max_id ~= entity_index.alive_count local will_recycle = entity_index.max_id ~= entity_index.alive_count
local e = world_entity(world) local e = world_entity(world)
if will_recycle then if will_recycle then
print(`*recycled {pe(e)}`) print(`*recycled {pe(e)}`)
else else
print(`*created {pe(e)}`) print(`*created {pe(e)}`)
end end
pad() pad()
return e return e
end end
w.print_entity_index = function(self) w.print_entity_index = function(self)
local max_id = entity_index.max_id local max_id = entity_index.max_id
local alive_count = entity_index.alive_count local alive_count = entity_index.alive_count
local alive = table.move(dense_array, 1+jecs.Rest::any, alive_count, 1, {}) local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})
local dead = table.move(dense_array, alive_count + 1, max_id, 1, {}) local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})
local sep = "|--------|" local sep = "|--------|"
if #alive > 0 then if #alive > 0 then
print("|-alive--|") print("|-alive--|")
for i = 1, #alive do for i = 1, #alive do
local e = pe(alive[i]) local e = pe(alive[i])
print_centered_entity(e, 32) print_centered_entity(e, 32)
print(sep) print(sep)
end end
print("\n") print("\n")
end end
if #dead > 0 then if #dead > 0 then
print("|--dead--|") print("|--dead--|")
for i = 1, #dead do for i = 1, #dead do
print_centered_entity(pe(dead[i]), 32) print_centered_entity(pe(dead[i]), 32)
print(sep) print(sep)
end end
end end
pad() pad()
end end
local timelines = {} local timelines = {}
w.print_snapshot = function(self) w.print_snapshot = function(self)
local timeline = #timelines + 1 local timeline = #timelines + 1
local entity_column_width = 10 local entity_column_width = 10
local status_column_width = 8 local status_column_width = 8
local header = string.format("| %-" .. entity_column_width .. "s |", "Entity") local header = string.format("| %-" .. entity_column_width .. "s |", "Entity")
for i = 1, timeline do for i = 1, timeline do
header = header .. string.format(" %-" .. status_column_width .. "s |", string.format("T%d", i)) header = header .. string.format(" %-" .. status_column_width .. "s |", string.format("T%d", i))
end end
local max_id = entity_index.max_id local max_id = entity_index.max_id
local alive_count = entity_index.alive_count local alive_count = entity_index.alive_count
local alive = table.move(dense_array, 1+jecs.Rest::any, alive_count, 1, {}) local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})
local dead = table.move(dense_array, alive_count + 1, max_id, 1, {}) local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})
local data = {} local data = {}
print("-------------------------------------------------------------------") print("-------------------------------------------------------------------")
print(header) print(header)
-- Store the snapshot data for this timeline -- Store the snapshot data for this timeline
for i = ENTITY_RANGE, max_id do for i = ENTITY_RANGE, max_id do
if dense_array[i] then if dense_array[i] then
local entity = dense_array[i] local entity = dense_array[i]
local id = ECS_ID(entity) local id = ECS_ID(entity)
local status = "alive" local status = "alive"
if not world:contains(entity) then if not world:contains(entity) then
status = "dead" status = "dead"
end end
data[id] = status data[id] = status
end end
end end
table.insert(timelines, data) table.insert(timelines, data)
-- Create a table to hold entity data for sorting -- Create a table to hold entity data for sorting
local entities = {} local entities = {}
for i = ENTITY_RANGE, max_id do for i = ENTITY_RANGE, max_id do
if dense_array[i] then if dense_array[i] then
local entity = dense_array[i] local entity = dense_array[i]
local id = ECS_ID(entity) local id = ECS_ID(entity)
-- Push entity and id into the new `entities` table -- Push entity and id into the new `entities` table
table.insert(entities, {entity = entity, id = id}) table.insert(entities, { entity = entity, id = id })
end end
end end
-- Sort the entities by ECS_ID -- Sort the entities by ECS_ID
table.sort(entities, function(a, b) table.sort(entities, function(a, b)
return a.id < b.id return a.id < b.id
end) end)
-- Print the sorted rows -- Print the sorted rows
for _, entity_data in ipairs(entities) do for _, entity_data in ipairs(entities) do
local entity = entity_data.entity local entity = entity_data.entity
local id = entity_data.id local id = entity_data.id
local status = "alive" local status = "alive"
if id > alive_count then if id > alive_count then
status = "dead" status = "dead"
end end
local row = string.format("| %-" .. entity_column_width .. "s |", pe(entity)) local row = string.format("| %-" .. entity_column_width .. "s |", pe(entity))
for j = 1, timeline do for j = 1, timeline do
local timeline_data = timelines[j] local timeline_data = timelines[j]
local entity_data = timeline_data[id] local entity_data = timeline_data[id]
if entity_data then if entity_data then
row = row .. string.format(" %-" .. status_column_width .. "s |", entity_data) row = row .. string.format(" %-" .. status_column_width .. "s |", entity_data)
else else
row = row .. string.format(" %-" .. status_column_width .. "s |", "-") row = row .. string.format(" %-" .. status_column_width .. "s |", "-")
end end
end end
print(row) print(row)
end end
print("-------------------------------------------------------------------") print("-------------------------------------------------------------------")
pad() pad()
end end
local world_add = world.add local world_add = world.add
local relations = {} local relations = {}
w.add = function(self, entity: any, component: any) w.add = function(self, entity: any, component: any)
world_add(world, entity, component) world_add(world, entity, component)
if jecs.IS_PAIR(component) then if jecs.IS_PAIR(component) then
local relation = jecs.pair_first(world, component) local relation = jecs.pair_first(world, component)
local target = jecs.pair_second(world, component) local target = jecs.pair_second(world, component)
print(`*added ({pe(relation)}, {pe(target)}) to {pe(entity)}`) print(`*added ({pe(relation)}, {pe(target)}) to {pe(entity)}`)
pad() pad()
end end
end
end
local world_delete = world.delete
local world_delete = world.delete w.delete = function(self, e)
w.delete = function(self, e) world_delete(world, e)
world_delete(world, e)
local idr_t = component_index[pair(__, e)]
local idr_t = component_index[pair(__, e)] if idr_t then
if idr_t then for archetype_id in idr_t.cache do
for archetype_id in idr_t.cache do local archetype = world.archetypes[archetype_id]
local archetype = world.archetypes[archetype_id] for _, id in archetype.types do
for _, id in archetype.types do if not jecs.IS_PAIR(id) then
if not jecs.IS_PAIR(id) then continue
continue end
end local object = jecs.pair_second(world, id)
local object = jecs.pair_second(world, id) if object ~= e then
if object ~= e then continue
continue end
end local id_record = component_index[id]
local id_record = component_index[id] local flags = id_record.flags
local flags = id_record.flags local flags_delete_mask: number = bit32.band(flags, jecs.ECS_ID_DELETE)
local flags_delete_mask: number = bit32.band(flags, jecs.ECS_ID_DELETE) if flags_delete_mask ~= 0 then
if flags_delete_mask ~= 0 then for _, entity in archetype.entities do
for _, entity in archetype.entities do print(`*deleted dependant {pe(entity)} of {pe(e)}`)
print(`*deleted dependant {pe(entity)} of {pe(e)}`) pad()
pad() end
end break
break else
else for _, entity in archetype.entities do
for _, entity in archetype.entities do print(
print(`*removed dependency ({pe(jecs.pair_first(world, id))}, {pe(object)}) from {pe(entity)}`) `*removed dependency ({pe(jecs.pair_first(world, id))}, {pe(object)}) from {pe(entity)}`
end )
end end
end end
end end
end end
end
print(`*deleted {pe(e)}`)
pad() print(`*deleted {pe(e)}`)
end pad()
return w end
end return w
end
return lifetime_tracker_add
return lifetime_tracker_add

153
tools/read_lcov.py Normal file
View file

@ -0,0 +1,153 @@
import os
LCOV_FILE = "coverage.out"
OUTPUT_DIR = "coverage"
os.makedirs(OUTPUT_DIR, exist_ok=True)
def parse_lcov(content):
"""Parses LCOV data from a single string."""
files = {}
current_file = None
for line in content.splitlines():
if line.startswith("SF:"):
current_file = line[3:].strip()
files[current_file] = {"coverage": {}, "functions": []}
elif line.startswith("DA:") and current_file:
parts = line[3:].split(",")
line_num = int(parts[0])
execution_count = int(parts[1])
files[current_file]["coverage"][line_num] = execution_count
elif line.startswith("FN:") and current_file:
parts = line[3:].split(",")
line_num = int(parts[0])
function_name = parts[1].strip()
files[current_file]["functions"].append({"name": function_name, "line": line_num, "hits": 0})
elif line.startswith("FNDA:") and current_file:
parts = line[5:].split(",")
hit_count = int(parts[0])
function_name = parts[1].strip()
for func in files[current_file]["functions"]:
if func["name"] == function_name:
func["hits"] = hit_count
break
return files
def read_source_file(filepath):
"""Reads source file content if available."""
if not os.path.exists(filepath):
return []
with open(filepath, "r", encoding="utf-8") as f:
return f.readlines()
def generate_file_html(filepath, coverage_data, functions_data):
"""Generates an HTML file for a specific source file."""
filename = os.path.basename(filepath)
source_code = read_source_file(filepath)
html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
total_hits = sum(func["hits"] for func in functions_data)
max_hits = max((func["hits"] for func in functions_data), default=0)
total_functions = len(functions_data)
covered_functions = sum(1 for func in functions_data if func["hits"] > 0)
function_coverage_percent = (covered_functions / total_functions * 100) if total_functions > 0 else 0
lines = [
"<html><head>",
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
'<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>',
"<style>",
"body { font-family: monospace; text-align: center; }",
"#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }",
"#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }",
"#funcTable th { background-color: #ddd; }",
"#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }",
".zero-hits { background-color: #fcc; font-weight: bold; color: red; }",
".nonzero-hits { color: green; font-weight: bold; }",
".low-hits { background-color: #ffe6b3; }",
".high-hits { background-color: #cfc; }",
".source-code-table { margin-left: 10px; }"
"th, td { padding: 0px; font-size: 12px; }",
"table.table { font-size: 14px; border-collapse: collapse; }",
"table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }",
"table.table tr { height: auto; }",
"</style></head><body>",
f'<h1 class="text-center">{filename} Coverage</h1>',
f'<h2>Total Execution Hits: {total_hits}</h2>',
f'<h2>Function Coverage Overview: {function_coverage_percent:.2f}%</h2>',
'<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">'
'Toggle Function Coverage</button>',
'<div class="collapse show" id="funcTable">',
'<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>'
]
longest_name = max((len(func["name"]) for func in functions_data), default=0)
for func in functions_data:
hit_color = "red" if func["hits"] == 0 else "green"
lines.append(
f'<tr><td style="padding: 1px; min-width: {longest_name}ch;">{func["name"]}</td>'
f'<td style="padding: 1px; color: {hit_color}; font-weight: bold;">{func["hits"]}</td></tr>'
)
lines.append('</tbody></table></div>') # Close collapsible div
lines.append('<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>')
for i, line in enumerate(source_code, start=1):
stripped_line = line.strip()
class_name = "text-muted"
if not stripped_line or stripped_line.startswith("end") or stripped_line.startswith("--"):
count_display = "<span class='text-muted'>N/A</span>"
lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{line.strip()}</td>></tr>')
else:
count = coverage_data.get(i, 0)
class_name = "zero-hits" if count == 0 else "low-hits" if count < max_hits * 0.3 else "high-hits"
count_display = f'{count}'
marked_text = f'<span class={class_name}>{line.strip()}</span>'
lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{marked_text}</td></tr>')
lines.append("</tbody></table></body></html>")
with open(html_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
def generate_index(files):
"""Generates an index.html summarizing the coverage."""
index_html = [
"<html><head>",
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
"</head><body>",
'<h1 class="text-center">Coverage Report</h1>',
'<table class="table table-striped table-bordered"><thead><tr><th>File</th><th>Total Hits</th><th>Functions</th></tr></thead><tbody>'
]
for filepath, data in files.items():
filename = os.path.basename(filepath)
total_hits = sum(func["hits"] for func in data["functions"])
total_functions = len(data["functions"])
index_html.append(f'<tr><td><a href="{filename}.html">{filename}</a></td><td>{total_hits}</td><td>{total_functions}</td></tr>')
index_html.append("</tbody></table></body></html>")
with open(os.path.join(OUTPUT_DIR, "index.html"), "w", encoding="utf-8") as f:
f.write("\n".join(index_html))
with open(LCOV_FILE, "r", encoding="utf-8") as f:
lcov_content = f.read()
files_data = parse_lcov(lcov_content)
for file_path, data in files_data.items():
generate_file_html(file_path, data["coverage"], data["functions"])
generate_index(files_data)
print(f"Coverage report generated in {OUTPUT_DIR}/index.html")

108
tools/runtime_lints.luau Normal file
View file

@ -0,0 +1,108 @@
local function dbg_info(n: number): any
return debug.info(n, "s")
end
local function throw(msg: string)
local s = 1
local root = dbg_info(1)
repeat
s += 1
until dbg_info(s) ~= root
if warn then
error(msg, s)
else
print(`[jecs] error: {msg}\n`)
end
end
local function ASSERT<T>(v: T, msg: string)
if v then
return
end
throw(msg)
end
local function runtime_lints_add(world)
local function get_name(id)
return world_get_one_inline(world, id, EcsName)
end
local function bname(id): string
local name: string
if ECS_IS_PAIR(id) then
local first = get_name(world, ecs_pair_first(world, id))
local second = get_name(world, ecs_pair_second(world, id))
name = `pair({first}, {second})`
else
return get_name(world, id)
end
if name then
return name
else
return `${id}`
end
end
local function ID_IS_TAG(world: World, id)
if ECS_IS_PAIR(id) then
id = ecs_pair_first(world, id)
end
return not world_has_one_inline(world, id, EcsComponent)
end
World.query = function(world: World, ...)
ASSERT((...), "Requires at least a single component")
return world_query(world, ...)
end
World.set = function(world: World, entity: i53, id: i53, value: any): ()
local is_tag = ID_IS_TAG(world, id)
if is_tag and value == nil then
local _1 = bname(world, entity)
local _2 = bname(world, id)
local why = "cannot set component value to nil"
throw(why)
return
elseif value ~= nil and is_tag then
local _1 = bname(world, entity)
local _2 = bname(world, id)
local why = `cannot set a component value because {_2} is a tag`
why ..= `\n[jecs] note: consider using "world:add({_1}, {_2})" instead`
throw(why)
return
end
world_set(world, entity, id, value)
end
World.add = function(world: World, entity: i53, id: i53, value: any)
if value ~= nil then
local _1 = bname(world, entity)
local _2 = bname(world, id)
throw("You provided a value when none was expected. " .. `Did you mean to use "world:add({_1}, {_2})"`)
end
world_add(world, entity, id)
end
World.get = function(world: World, entity: i53, ...)
local length = select("#", ...)
ASSERT(length < 5, "world:get does not support more than 4 components")
local _1
for i = 1, length do
local id = select(i, ...)
local id_is_tag = not world_has(world, id, EcsComponent)
if id_is_tag then
local name = get_name(world, id)
if not _1 then
_1 = get_name(world, entity)
end
throw(
`cannot get (#{i}) component {name} value because it is a tag.`
.. `\n[jecs] note: If this was intentional, use "world:has({_1}, {name}) instead"`
)
end
end
return world_get(world, entity, ...)
end
end

View file

@ -222,37 +222,16 @@ local function CHECK<T>(value: T, stack: number?): T?
end end
local function TEST(name: string, fn: () -> ()) local function TEST(name: string, fn: () -> ())
local active = test
assert(not active, "cannot start test while another test is in progress")
test = { test = {
name = name, name = name,
cases = {}, cases = {},
duration = 0, duration = 0,
focus = false, focus = false,
fn = fn
} }
assert(test)
table.insert(tests, test) table.insert(tests, test)
local start = os.clock()
local err
local success = xpcall(fn, function(m: string)
err = { message = m, trace = debug.traceback(nil, 2) }
end)
test.duration = os.clock() - start
if not test.case then
CASE("")
end
assert(test.case, "no active case")
if not success then
test.case.result = ERROR
test.error = err
end
test = nil
end end
local function FOCUS() local function FOCUS()
@ -274,6 +253,31 @@ local function FINISH(): boolean
local total_focus_cases = 0 local total_focus_cases = 0
local duration = 0 local duration = 0
for _, t in tests do
if check_for_focused and not t.focus then
continue
end
test = t
fn = t.fn
local start = os.clock()
local err
local success = xpcall(fn, function(m: string)
err = { message = m, trace = debug.traceback(nil, 2) }
end)
test.duration = os.clock() - start
if not test.case then
CASE("")
end
assert(test.case, "no active case")
if not success then
test.case.result = ERROR
test.error = err
end
collectgarbage()
end
for _, test in tests do for _, test in tests do
duration += test.duration duration += test.duration
for _, case in test.cases do for _, case in test.cases do
@ -534,7 +538,7 @@ return {
FINISH = FINISH, FINISH = FINISH,
SKIP = SKIP, SKIP = SKIP,
FOCUS = FOCUS, FOCUS = FOCUS,
CHECK_EXPECT_ERR = CHECK_EXPECT_ERR CHECK_EXPECT_ERR = CHECK_EXPECT_ERR,
} }
end, end,