diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 03e3b194..cdaf8f1f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -40,6 +40,19 @@ jobs: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: goveralls -coverprofile=/var/tmp/capillaries.p -service=github + - name: pkg/capigraph test coverage threshold check + env: + TESTCOVERAGE_THRESHOLD: 93.9 + run: | + go test -v ./pkg/capigraph/... -coverprofile coverage.out -covermode count + totalCoverage=`go tool cover -func=coverage.out | grep total | grep -Eo '[0-9]+\.[0-9]+'` + if (( $(echo "$totalCoverage $TESTCOVERAGE_THRESHOLD" | awk '{print ($1 >= $2)}') )); then + echo -e "\033[32mOK: $totalCoverage >= $TESTCOVERAGE_THRESHOLD\033[0m" + else + echo -e "\033[31mFAILED: $totalCoverage < $TESTCOVERAGE_THRESHOLD. Cover more with unit tests or adjust threshold to a lower value.\033[0m" + exit 1 + fi + - name: pkg/cql test coverage threshold check env: TESTCOVERAGE_THRESHOLD: 87.6 diff --git a/.golangci.yml b/.golangci.yml index 977c7ada..f9f56a4e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -126,3 +126,30 @@ issues: text: use of `fmt.Print(|f|ln)` forbidden by pattern linters: - forbidigo + - path: pkg\/capigraph\/svg_test\.go + text: use of `fmt.Print(|f|ln)` forbidden by pattern + linters: + - forbidigo + - path: pkg\/capigraph\/layering\.go + text: function buildLayerMap has cognitive complexity + linters: + - revive + - path: pkg\/capigraph\/layer_permutator\.go + text: function \(\*LayerPermutator\)\.swapPermutationByIdx has cognitive complexity + linters: + - revive + - text: struct literal uses unkeyed fields + linters: + - govet + - path: pkg\/api\/capigraph\.go + text: function GetCapigraphDiagram has (cognitive|cyclomatic) complexity + linters: + - revive + - path: pkg\/capigraph\/svg\.go + text: maximum number of return results per function exceeded + linters: + - revive + - path: pkg\/capigraph\/viz_node_hierarchy\.go + text: maximum number of return results per function exceeded + linters: + - revive diff --git a/.vscode/launch.json b/.vscode/launch.json index 7fddf6fe..fca69429 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -80,13 +80,22 @@ "args": [] }, { - "name": "Toolbelt", + "name": "Toolbelt validate_script", "type": "go", "request": "launch", "mode": "debug", "cwd":"${workspaceFolder}/pkg/exe/toolbelt", "program": "${workspaceFolder}/pkg/exe/toolbelt/capitoolbelt.go", - "args": ["start_run", "-script_file=/tmp/capi_cfg/portfolio_quicktest/script.json", "-params_file=/tmp/capi_cfg/portfolio_quicktest/script_params.json", "-keyspace=portfolio_quicktest", "-start_nodes=1_read_accounts,1_read_txns,1_read_period_holdings"] + "args": ["validate_script", "-script_file=/tmp/capi_cfg/fannie_mae_quicktest/script.json", "-params_file=/tmp/capi_cfg/fannie_mae_quicktest/script_params.json", "-format=capigraph", "-detail="] + }, + { + "name": "Toolbelt get_run_status_diagram", + "type": "go", + "request": "launch", + "mode": "debug", + "cwd":"${workspaceFolder}/pkg/exe/toolbelt", + "program": "${workspaceFolder}/pkg/exe/toolbelt/capitoolbelt.go", + "args": ["get_run_status_diagram", "-script_file=/tmp/capi_cfg/fannie_mae_quicktest/script.json", "-params_file=/tmp/capi_cfg/fannie_mae_quicktest/script_params.json", "-keyspace=fannie_mae_quicktest", "-run_id=1", "-format=capigraph"] }, { "name": "Webapi", diff --git a/doc/binconfig.md b/doc/binconfig.md index bc5fdb10..4558f9c1 100644 --- a/doc/binconfig.md +++ b/doc/binconfig.md @@ -73,7 +73,7 @@ Default: 5 threads Path to the directory containing PEM certificates for all supported CAs. Required only if any of the following is referenced by HTTPS: - script file - script parameter file -- [tag_criteria_uri](glossary.md#tag_criteria_uri) +- [tag_criteria_url](glossary.md#tag_criteria_url) - input data files To obtain the PEM cert, navigate to the file URI with a browser, open certificate information, navigate to the root certificate, save it as DER/CER (say, digicert.cer), and convert it to pem using this command: diff --git a/doc/dot-fanniemae.svg b/doc/dot-fanniemae.svg deleted file mode 100755 index 9cafd625..00000000 --- a/doc/dot-fanniemae.svg +++ /dev/null @@ -1,331 +0,0 @@ - - -indexes - - - -deal_seller_summaries - - -05_deal_seller_summaries - -creates table: -deal_seller_summaries - -group:true, join:left - - - - - -05_write_file_deal_seller_summaries - - -05_write_file_deal_seller_summaries - -creates file: -/tmp/capi_out/fannie_mae_quicktest/deal_seller_summaries.parquet - - - - - -deal_seller_summaries->05_write_file_deal_seller_summaries - - -deal_seller_summaries (no parallelism) - - - -/tmp/capi_out/fannie_mae_quicktest/deal_seller_summaries.parquet - - - - -/tmp/capi_out/fannie_mae_quicktest/deal_seller_summaries.parquet - - - - - -05_write_file_deal_seller_summaries->/tmp/capi_out/fannie_mae_quicktest/deal_seller_summaries.parquet - - - - - -deal_total_upbs - - -03_deal_total_upbs - -creates table: -deal_total_upbs - -group:true, join:left - - - - - -deal_summaries - - -05_deal_summaries - -creates table: -deal_summaries - -group:true, join:left - - - - - -deal_total_upbs->deal_summaries - - -deal_total_upbs (10 batches) - - - -05_write_file_deal_summaries - - -05_write_file_deal_summaries - -creates file: -/tmp/capi_out/fannie_mae_quicktest/deal_summaries.parquet - - - - - -deal_summaries->05_write_file_deal_summaries - - -deal_summaries (no parallelism) - - - -loan_summaries_calculated - - -04_loan_summaries_calculated - -creates table: -loan_summaries_calculated - - - - - -loan_summaries_calculated->deal_seller_summaries - - -idx_loan_summaries_calculated_deal_name_seller_name (lookup) - - - -loan_summaries_calculated->deal_summaries - - -idx_loan_summaries_calculated_deal_name (lookup) - - - -04_write_file_loan_summaries_calculated - - -04_write_file_loan_summaries_calculated - -creates file: -/tmp/capi_out/fannie_mae_quicktest/loan_summaries_calculated.parquet - - - - - -loan_summaries_calculated->04_write_file_loan_summaries_calculated - - -loan_summaries_calculated (no parallelism) - - - -loan_ids - - -02_loan_ids - -creates table: -loan_ids - - - - - -loan_ids->deal_total_upbs - - -idx_loan_ids_deal_name (lookup) - - - -deal_names - - -02_deal_names - -creates table: -deal_names - - - - - -loan_ids->deal_names - - -loan_ids (10 batches) - - - -deal_sellers - - -02_deal_sellers - -creates table: -deal_sellers - - - - - -loan_ids->deal_sellers - - -loan_ids (10 batches) - - - -loan_payment_summaries - - -04_loan_payment_summaries - -creates table: -loan_payment_summaries - -group:true, join:left - - - - - -loan_ids->loan_payment_summaries - - -loan_ids (10 batches) - - - -deal_names->deal_total_upbs - - -deal_names (10 batches) - - - -deal_sellers->deal_seller_summaries - - -deal_sellers (10 batches) - - - -loan_payment_summaries->loan_summaries_calculated - - -loan_payment_summaries (10 batches) - - - -/tmp/capi_out/fannie_mae_quicktest/loan_summaries_calculated.parquet - - - - -/tmp/capi_out/fannie_mae_quicktest/loan_summaries_calculated.parquet - - - - - -04_write_file_loan_summaries_calculated->/tmp/capi_out/fannie_mae_quicktest/loan_summaries_calculated.parquet - - - - - -/tmp/capi_in/fannie_mae_quicktest/CAS_2023_R08_G1_20231020_000.parquet - - -/tmp/capi_in/fannie_mae_quicktest/CAS_2023_R08_G1_20231020_000.parquet -/tmp/capi_in/fannie_mae_quicktest/CAS_2023_R08_G1_20231020_001.parquet - - - - - -payments - - -01_read_payments - -creates table: -payments - - - - - -/tmp/capi_in/fannie_mae_quicktest/CAS_2023_R08_G1_20231020_000.parquet->payments - - - - - -payments->loan_ids - - -payments (10 batches) - - - -payments->loan_payment_summaries - - -idx_payments_by_loan_id (lookup) - - - -/tmp/capi_out/fannie_mae_quicktest/deal_summaries.parquet - - - - -/tmp/capi_out/fannie_mae_quicktest/deal_summaries.parquet - - - - - -05_write_file_deal_summaries->/tmp/capi_out/fannie_mae_quicktest/deal_summaries.parquet - - - - - \ No newline at end of file diff --git a/doc/dot-lookup.svg b/doc/dot-lookup.svg deleted file mode 100644 index 5cfb081f..00000000 --- a/doc/dot-lookup.svg +++ /dev/null @@ -1,317 +0,0 @@ - - -indexes - - - -orders - - -read_orders - -creates table: -orders - - - - - -order_date_value_grouped_inner - - -order_date_value_grouped_inner - -creates table: -order_date_value_grouped_inner - -group:true, join:inner - - - - - -orders->order_date_value_grouped_inner - - -orders (10 batches) - - - -order_date_value_grouped_left_outer - - -order_date_value_grouped_left_outer - -creates table: -order_date_value_grouped_left_outer - -group:true, join:left - - - - - -orders->order_date_value_grouped_left_outer - - -orders (10 batches) - - - -order_item_date_inner - - -order_item_date_inner - -creates table: -order_item_date_inner - -group:false, join:inner - - - - - -orders->order_item_date_inner - - -orders (10 batches) - - - -order_item_date_left_outer - - -order_item_date_left_outer - -creates table: -order_item_date_left_outer - -group:false, join:left - - - - - -orders->order_item_date_left_outer - - -orders (10 batches) - - - -file_order_date_value_grouped_inner - - -file_order_date_value_grouped_inner - -creates file: -/tmp/capi_out/lookup/order_date_value_grouped_inner.csv - - - - - -order_date_value_grouped_inner->file_order_date_value_grouped_inner - - -order_date_value_grouped_inner (no parallelism) - - - -order_items - - -read_order_items - -creates table: -order_items - - - - - -order_items->order_date_value_grouped_inner - - -idx_order_items_order_id (lookup) - - - -order_items->order_date_value_grouped_left_outer - - -idx_order_items_order_id (lookup) - - - -order_items->order_item_date_inner - - -idx_order_items_order_id (lookup) - - - -order_items->order_item_date_left_outer - - -idx_order_items_order_id (lookup) - - - -/tmp/capi_out/lookup/order_date_value_grouped_inner.csv - - - - -/tmp/capi_out/lookup/order_date_value_grouped_inner.csv - - - - - -file_order_date_value_grouped_inner->/tmp/capi_out/lookup/order_date_value_grouped_inner.csv - - - - - -file_order_date_value_grouped_left_outer - - -file_order_date_value_grouped_left_outer - -creates file: -/tmp/capi_out/lookup/order_date_value_grouped_left_outer.csv - - - - - -order_date_value_grouped_left_outer->file_order_date_value_grouped_left_outer - - -order_date_value_grouped_left_outer (no parallelism) - - - -/tmp/capi_out/lookup/order_date_value_grouped_left_outer.csv - - - - -/tmp/capi_out/lookup/order_date_value_grouped_left_outer.csv - - - - - -file_order_date_value_grouped_left_outer->/tmp/capi_out/lookup/order_date_value_grouped_left_outer.csv - - - - - -/tmp/capi_in/lookup/olist_orders_dataset.csv - - -/tmp/capi_in/lookup/olist_orders_dataset.csv - - - - - -/tmp/capi_in/lookup/olist_orders_dataset.csv->orders - - - - - -/tmp/capi_in/lookup/olist_order_items_dataset.csv - - -/tmp/capi_in/lookup/olist_order_items_dataset.csv - - - - - -/tmp/capi_in/lookup/olist_order_items_dataset.csv->order_items - - - - - -file_order_item_date_inner - - -file_order_item_date_inner - -creates file: -/tmp/capi_out/lookup/order_item_date_inner.csv - - - - - -order_item_date_inner->file_order_item_date_inner - - -order_item_date_inner (no parallelism) - - - -file_order_item_date_left_outer - - -file_order_item_date_left_outer - -creates file: -/tmp/capi_out/lookup/order_item_date_left_outer.csv - - - - - -order_item_date_left_outer->file_order_item_date_left_outer - - -order_item_date_left_outer (no parallelism) - - - -/tmp/capi_out/lookup/order_item_date_left_outer.csv - - - - -/tmp/capi_out/lookup/order_item_date_left_outer.csv - - - - - -file_order_item_date_left_outer->/tmp/capi_out/lookup/order_item_date_left_outer.csv - - - - - -/tmp/capi_out/lookup/order_item_date_inner.csv - - - - -/tmp/capi_out/lookup/order_item_date_inner.csv - - - - - -file_order_item_date_inner->/tmp/capi_out/lookup/order_item_date_inner.csv - - - - - \ No newline at end of file diff --git a/doc/dot-proto-file-reader-creator.svg b/doc/dot-proto-file-reader-creator.svg deleted file mode 100755 index ae162580..00000000 --- a/doc/dot-proto-file-reader-creator.svg +++ /dev/null @@ -1,70 +0,0 @@ - - -indexes - - - -/tmp/capi_in/proto_file_reader_creator_quicktest/products.csv - - -/tmp/capi_in/proto_file_reader_creator_quicktest/products.csv - - - - - -products_csv - - -read_file - -creates table: -products_csv - - - - - -/tmp/capi_in/proto_file_reader_creator_quicktest/products.csv->products_csv - - - - - -write_file - - -write_file - -creates file: -/tmp/capi_out/proto_file_reader_creator_quicktest/products.csv - - - - - -products_csv->write_file - - -products_csv (no parallelism) - - - -/tmp/capi_out/proto_file_reader_creator_quicktest/products.csv - - - - -/tmp/capi_out/proto_file_reader_creator_quicktest/products.csv - - - - - -write_file->/tmp/capi_out/proto_file_reader_creator_quicktest/products.csv - - - - - \ No newline at end of file diff --git a/doc/dot-pycalc.svg b/doc/dot-pycalc.svg deleted file mode 100644 index 591b88b3..00000000 --- a/doc/dot-pycalc.svg +++ /dev/null @@ -1,148 +0,0 @@ - - -indexes - - - -taxed_order_items_go - - -taxed_order_items_go - -creates table: -taxed_order_items_go - - - - - -file_taxed_order_items_go - - -file_taxed_order_items_go - -creates file: -/tmp/capi_out/py_calc/taxed_order_items_go_{batch_idx|string}.csv - - - - - -taxed_order_items_go->file_taxed_order_items_go - - -taxed_order_items_go (2 batches) - - - -/tmp/capi_out/py_calc/taxed_order_items_go_{batch_idx|string}.csv - - - - -/tmp/capi_out/py_calc/taxed_order_items_go_{batch_idx|string}.csv - - - - - -file_taxed_order_items_go->/tmp/capi_out/py_calc/taxed_order_items_go_{batch_idx|string}.csv - - - - - -order_items - - -read_order_items - -creates table: -order_items - - - - - -order_items->taxed_order_items_go - - -order_items (10 batches) - - - -taxed_order_items_py - - -taxed_order_items_py - -creates table: -taxed_order_items_py - - - - - -order_items->taxed_order_items_py - - -order_items (10 batches) - - - -file_taxed_order_items_py - - -file_taxed_order_items_py - -creates file: -/tmp/capi_out/py_calc/taxed_order_items_py_{batch_idx|string}.csv - - - - - -taxed_order_items_py->file_taxed_order_items_py - - -taxed_order_items_py (2 batches) - - - -/tmp/capi_out/py_calc/taxed_order_items_py_{batch_idx|string}.csv - - - - -/tmp/capi_out/py_calc/taxed_order_items_py_{batch_idx|string}.csv - - - - - -file_taxed_order_items_py->/tmp/capi_out/py_calc/taxed_order_items_py_{batch_idx|string}.csv - - - - - -/tmp/capi_in/py_calc/olist_order_items_dataset00.csv - - -/tmp/capi_in/py_calc/olist_order_items_dataset00.csv -/tmp/capi_in/py_calc/olist_order_items_dataset01.csv -/tmp/capi_in/py_calc/olist_order_items_dataset02.csv -/tmp/capi_in/py_calc/olist_order_items_dataset03.csv -/tmp/capi_in/py_calc/olist_order_items_dataset04.csv - - - - - -/tmp/capi_in/py_calc/olist_order_items_dataset00.csv->order_items - - - - - \ No newline at end of file diff --git a/doc/dot-tag-and-denormalize.svg b/doc/dot-tag-and-denormalize.svg deleted file mode 100644 index ad1b899b..00000000 --- a/doc/dot-tag-and-denormalize.svg +++ /dev/null @@ -1,180 +0,0 @@ - - -indexes - - - -/tmp/capi_in/tag_and_denormalize/tags.csv - - -/tmp/capi_in/tag_and_denormalize/tags.csv - - - - - -tags - - -read_tags - -creates table: -tags - - - - - -/tmp/capi_in/tag_and_denormalize/tags.csv->tags - - - - - -tag_totals - - -tag_totals - -creates table: -tag_totals - -group:true, join:left - - - - - -tags->tag_totals - - -tags (10 batches) - - - -/tmp/capi_in/tag_and_denormalize/flipcart_products.tsv - - -/tmp/capi_in/tag_and_denormalize/flipcart_products.tsv - - - - - -products - - -read_products - -creates table: -products - - - - - -/tmp/capi_in/tag_and_denormalize/flipcart_products.tsv->products - - - - - -tagged_products - - -tag_products - -creates table: -tagged_products - - - - - -products->tagged_products - - -products (10 batches) - - - -file_tagged_products - - -file_tagged_products - -creates file: -/tmp/capi_out/tag_and_denormalize/tagged_products_for_operator_review.csv - - - - - -tagged_products->file_tagged_products - - -tagged_products (no parallelism) - - - -tagged_products->tag_totals - - -idx_tagged_products_tag (lookup) - - - -/tmp/capi_out/tag_and_denormalize/tagged_products_for_operator_review.csv - - - - -/tmp/capi_out/tag_and_denormalize/tagged_products_for_operator_review.csv - - - - - -file_tagged_products->/tmp/capi_out/tag_and_denormalize/tagged_products_for_operator_review.csv - - - - - -file_tag_totals - - -file_tag_totals - -creates file: -/tmp/capi_out/tag_and_denormalize/tag_totals.tsv - - - - - -tag_totals->file_tag_totals - - -tag_totals (no parallelism) - - - -/tmp/capi_out/tag_and_denormalize/tag_totals.tsv - - - - -/tmp/capi_out/tag_and_denormalize/tag_totals.tsv - - - - - -file_tag_totals->/tmp/capi_out/tag_and_denormalize/tag_totals.tsv - - - - - \ No newline at end of file diff --git a/doc/glossary.md b/doc/glossary.md index 32de72c9..08da7d83 100644 --- a/doc/glossary.md +++ b/doc/glossary.md @@ -118,7 +118,7 @@ The field in the target table where the tag value will be written to tag->criteria_expression map. [Expressions](#go-expressions) are allowed to use reader fields only (`r.*`). -### tag_criteria_uri +### tag_criteria_url Same as [tag_criteria](#tag_criteria), but in a separate JSON file. This is the preferred method to specify tag criteria because the list of tags: - may contain thousands of entries, it's not a good idea to pollute the script file with those @@ -145,10 +145,6 @@ At the moment, Capillaries supports only a limited subset of the standard Go lib RabbitMQ queue containing messages for a [processor](#processor). The name of the queue is given by the [handler_executable_type](binconfig.md#handler_executable_type) setting. -## DOT diagrams - -[Graphviz DOT language](https://graphviz.org/) - markdown-style diagram-drawing language. There is a number of free online tools that can visualize DOT language documents. - ## Toolbelt A command-line executable that performs common Capillaries operations by: - reading Capillaries [script files](#script) @@ -157,18 +153,18 @@ A command-line executable that performs common Capillaries operations by: The Toolbelt: - can [start/stop](api.md) [runs](#run), so solution developers can use it in their scripts - gives very basic access to the [workflow tables](#workflow-table), see `get_*_history` commands -- can produce rudimentary visuals using [DOT diagram language](#dot-diagrams) - see `validate_script`, `get_run_status_diagram` commands +- can produce visual diagrams - see `validate_script`, `get_run_status_diagram` commands See [Toolbelt and Daemon configuration](binconfig.md) for configuration settings. -One of the main purposes of the toolbelt is to give system integrators easy access to [Capillaries API](api.md). Also, the toolbelt can be useful for visualizing [scripts](#script) and the status of their execution with [DOT diagrams](#dot-diagrams), for example: +One of the main purposes of the toolbelt is to give system integrators easy access to [Capillaries API](api.md). Also, the toolbelt can be useful for visualizing [scripts](#script) and the status of their execution with diagrams, for example: ``` # Can be executed anytime -go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/lookup_quicktest/script.json -params_file=../../../test/data/cfg/lookup_quicktest/script_params_two_runs.json -idx_dag=true +go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/lookup_quicktest/script.json -params_file=../../../test/data/cfg/lookup_quicktest/script_params_two_runs.json -detail=idx # Can be executed when the lookup script is running using two runs -go run capitoolbelt.go get_run_status_diagram -script_file=../../../test/data/cfg/lookup_quicktest/script.json -params_file=../../../test/data/cfg/lookup_quicktest/script_params_two_runs.json -keyspace=lookup_quicktest -run_id=1 +go run capitoolbelt.go get_run_status_diagram -script_file=../../../test/data/cfg/lookup_quicktest/script.json -params_file=../../../test/data/cfg/lookup_quicktest/script_params_two_runs.json -keyspace=lookup_quicktest_one_run_json -run_id=1 ``` ## Deploy tool diff --git a/doc/qna.md b/doc/qna.md index b3fc0961..647d0f58 100644 --- a/doc/qna.md +++ b/doc/qna.md @@ -75,7 +75,7 @@ Q. Is there a UI for Capillaries? A. Yes. See [Capillaries UI](../ui/README.md) project, which is a simple web single-page application that shows the status of every [run](glossary.md#run) in every [keyspace](glossary.md#keyspace). UI requirements tend to be very business-specific, it's not an easy task to come up with a cookie-cutter UI framework that would be flexible enough. Dedicated solution developers are encouraged to develop their own UI for Capillaries workflows, using [Capillaries Webapi](glossary.md#webapi) as a back-end and [Capillaries UI](../ui/README.md) as an example. -Also please note that [Toolbelt](glossary.md#toolbelt) can produce rudimentary visuals using [DOT diagram language](glossary.md#dot-diagrams) - see [Toolbelt](glossary.md#toolbelt) `validate_script`, `get_run_status_diagram` commands. +Also please note that [Toolbelt](glossary.md#toolbelt) can produce visual diagrams - see [Toolbelt](glossary.md#toolbelt) `validate_script`, `get_run_status_diagram` commands. ## Can Capillaries run in a Docker container? diff --git a/doc/scriptconfig.md b/doc/scriptconfig.md index bb8db878..5462f9a3 100644 --- a/doc/scriptconfig.md +++ b/doc/scriptconfig.md @@ -40,7 +40,7 @@ Node [processor type](glossary.md#processor-types) ### start_policy - `auto`: Capillaries automatically start this node processing when all dependency nodes are successfully completed -- `manual`: Capillaries will start this node processing only if this node is explicitly specified on the [run](glossary.md#run) start; manual nodes are marked on the [dot diagram](dot-tag-and-denormalize.svg) with a thicker border +- `manual`: Capillaries will start this node processing only if this node is explicitly specified on the [run](glossary.md#run) start; manual nodes are marked on the [diagram](viz-tag-and-denormalize.svg) with a thicker border Mark nodes as `manual` when you want the operator to review the results of the previous [runs](glossary.md#run) before moving ahead with the rest of the script. diff --git a/doc/viz-fanniemae.svg b/doc/viz-fanniemae.svg new file mode 100755 index 00000000..bcb19adc --- /dev/null +++ b/doc/viz-fanniemae.svg @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 01_read_payments + Read source files to a table + Processor: read from files into a table + File(s): /tmp/capi_in/.../CAS_2023_R08_G1_20231020_000.parquet + Table created: payments + + + + + + + + 02_loan_ids + Select distinct loan ids + Processor: select distinct rows + Unique index: idx_loan_ids_loan_id + Table created: loan_ids + + + + + payments + (10 batches) + + + + + + + + 02_deal_names + Select distinct deal names + Processor: select distinct rows + Unique index: idx_deal_names_deal_name + Table created: deal_names + + + + + loan_ids + (10 batches) + + + + + + + + 03_deal_total_upbs + For each deal, calculate total UPBs + Processor: left join with lookup table, group: true + Lookup index: idx_loan_ids_deal_name + Table created: deal_total_upbs + + + + + deal_names + (10 batches) + + + + + idx_loan_ids_deal_name + (lookup) + + + + + + + + 05_deal_summaries + For each deal, calculate aggregates from calculated loan summaries + Processor: left join with lookup table, group: true + Lookup index: idx_loan_summaries_calculated_deal_name + Table created: deal_summaries + + + + + deal_total_upbs + (10 batches) + + + + + idx_loan_summaries_calculated_deal_name + (lookup) + + + + + + + + 05_write_file_deal_summaries + Write from table to file deal_summaries.parquet + Processor: read from table into files + Source table: deal_summaries + File(s): /tmp/capi_out/.../deal_summaries.parquet + + + + + deal_summaries + (no parallelism) + + + + + + + + 04_loan_payment_summaries + For each loan, merge all payments into single json string + Processor: left join with lookup table, group: true + Lookup index: idx_payments_by_loan_id + Table created: loan_payment_summaries + + + + + loan_ids + (10 batches) + + + + + idx_payments_by_loan_id + (lookup) + + + + + + + + 04_loan_summaries_calculated + Apply Python calculations to loan summaries + Processor: apply Python calculations + Python file(s): /tmp/capi_cfg/.../payment_calc.py + Table created: loan_summaries_calculated + + + + + loan_payment_summaries + (10 batches) + + + + + + + + 04_write_file_loan_summaries_calculated + Write from table to file loan_summaries.parquet + Processor: read from table into files + Source table: loan_summaries_calculated + File(s): /tmp/capi_out/.../loan_summaries_calculated.parquet + + + + + loan_summaries_calculated + (no parallelism) + + + + + + + + 02_deal_sellers + Select distinct sellers + Processor: select distinct rows + Unique index: idx_sellers_deal_name_seller_name + Table created: deal_sellers + + + + + loan_ids + (10 batches) + + + + + + + + 05_deal_seller_summaries + For each deal/seller, calculate aggregates from calculated loan summaries + Processor: left join with lookup table, group: true + Lookup index: idx_loan_summaries_calculated_deal_name_seller_name + Table created: deal_seller_summaries + + + + + deal_sellers + (10 batches) + + + + + idx_loan_summaries_calculated_deal_name_seller_name + (lookup) + + + + + + + + 05_write_file_deal_seller_summaries + Write from table to file deal_seller_summaries.parquet + Processor: read from table into files + Source table: deal_seller_summaries + File(s): /tmp/capi_out/.../deal_seller_summaries.parquet + + + + + deal_seller_summaries + (no parallelism) + +Perms 6, elapsed 0.000s, dist 2707.6 + diff --git a/doc/viz-lookup.svg b/doc/viz-lookup.svg new file mode 100644 index 00000000..dd458fa0 --- /dev/null +++ b/doc/viz-lookup.svg @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + read_order_items + Load order item data from CSV file + Processor: read from files into a table + File(s): /tmp/capi_in/.../olist_order_items_dataset.csv + Table created: order_items + + + + + + + + read_orders + Load order data from CSV file + Processor: read from files into a table + File(s): /tmp/capi_in/.../olist_orders_dataset.csv + Table created: orders + + + + + + + + order_date_value_grouped_inner + For each order, look up order items and perform inner join with grouping and aggregation + Processor: inner join with lookup table, group: true + Lookup index: idx_order_items_order_id + Table created: order_date_value_grouped_inner + + + + + orders + (10 batches) + + + + + idx_order_items_order_id + (lookup) + + + + + + + + file_order_date_value_grouped_inner + Write the results of inner join with grouping to CSV file + Processor: read from table into files + Source table: order_date_value_grouped_inner + File(s): /tmp/capi_out/.../order_date_value_grouped_inner.csv + + + + + order_date_value_grouped_inner + (no parallelism) + + + + + + + + order_item_date_inner + For each order, look up order items and perform inner join without grouping or aggregation + Processor: inner join with lookup table, group: false + Lookup index: idx_order_items_order_id + Table created: order_item_date_inner + + + + + orders + (10 batches) + + + + + idx_order_items_order_id + (lookup) + + + + + + + + file_order_item_date_inner + Write the results of inner join without grouping to CSV file + Processor: read from table into files + Source table: order_item_date_inner + File(s): /tmp/capi_out/.../order_item_date_inner.csv + + + + + order_item_date_inner + (no parallelism) + + + + + + + + order_date_value_grouped_left_outer + For each order, look up order items and perform left outer join with grouping and aggregation + Processor: left join with lookup table, group: true + Lookup index: idx_order_items_order_id + Table created: order_date_value_grouped_left_outer + + + + + orders + (10 batches) + + + + + idx_order_items_order_id + (lookup) + + + + + + + + file_order_date_value_grouped_left_outer + Write the results of left outer join with grouping to CSV file + Processor: read from table into files + Source table: order_date_value_grouped_left_outer + File(s): /tmp/capi_out/.../order_date_value_grouped_left_outer.csv + + + + + order_date_value_grouped_left_outer + (no parallelism) + + + + + + + + order_item_date_left_outer + For each order, look up order items and perform left outer join without grouping or aggregation + Processor: left join with lookup table, group: false + Lookup index: idx_order_items_order_id + Table created: order_item_date_left_outer + + + + + orders + (10 batches) + + + + + idx_order_items_order_id + (lookup) + + + + + + + + file_order_item_date_left_outer + Write the results of left oter join without grouping to CSV file + Processor: read from table into files + Source table: order_item_date_left_outer + File(s): /tmp/capi_out/.../order_item_date_left_outer.csv + + + + + order_item_date_left_outer + (no parallelism) + +Perms 48, elapsed 0.000s, dist 9751.2 + diff --git a/doc/viz-proto-file-reader-creator.svg b/doc/viz-proto-file-reader-creator.svg new file mode 100755 index 00000000..9d64101e --- /dev/null +++ b/doc/viz-proto-file-reader-creator.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + read_file + Read file /tmp/capi_in/proto_file_reader_creator_quicktest/products.csv to table + Processor: read from files into a table + File(s): /tmp/capi_in/.../products.csv + Table created: products_csv + + + + + + + + write_file + Write from table to file /tmp/capi_out/proto_file_reader_creator_quicktest/products.csv + Processor: read from table into files + Source table: products_csv + File(s): /tmp/capi_out/.../products.csv + + + + + products_csv + (no parallelism) + +Perms 1, elapsed 0.000s, dist 0.0 + diff --git a/doc/viz-pycalc.svg b/doc/viz-pycalc.svg new file mode 100644 index 00000000..df608531 --- /dev/null +++ b/doc/viz-pycalc.svg @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + read_order_items + Load order item data from CSV files to a table, one input file - one batch + Processor: read from files into a table + File(s): /tmp/capi_in/.../olist_order_items_dataset00.csv + Table created: order_items + + + + + + + + taxed_order_items_go + Apply Go-based calculations to order items + Processor: write from table to table + Source table: order_items + Table created: taxed_order_items_go + + + + + order_items + (10 batches) + + + + + + + + file_taxed_order_items_go + Write Go-based calculations to CSV file, batch by batch + Processor: read from table into files + Source table: taxed_order_items_go + File(s): /tmp/capi_out/.../taxed_order_items_go_{batch_idx|string}.csv + + + + + taxed_order_items_go + (2 batches) + + + + + + + + taxed_order_items_py + Apply Python-based calculations to order items + Processor: apply Python calculations + Python file(s): /tmp/capi_cfg/.../calc_order_items_code.py + Table created: taxed_order_items_py + + + + + order_items + (10 batches) + + + + + + + + file_taxed_order_items_py + Write Python-based calculations to CSV file, batch by batch + Processor: read from table into files + Source table: taxed_order_items_py + File(s): /tmp/capi_out/.../taxed_order_items_py_{batch_idx|string}.csv + + + + + taxed_order_items_py + (2 batches) + +Perms 2, elapsed 0.000s, dist 0.0 + diff --git a/doc/viz-tag-and-denormalize.svg b/doc/viz-tag-and-denormalize.svg new file mode 100644 index 00000000..1831344c --- /dev/null +++ b/doc/viz-tag-and-denormalize.svg @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + read_products + Load product data from CSV files to a table, one input file - one batch + Processor: read from files into a table + File(s): /tmp/capi_in/.../flipcart_products.tsv + Table created: products + + + + + + + + tag_products + Tag products according to criteria and write product tag, id, price to a new table + Processor: tag and denormalize + Tag criteria URL: /tmp/capi_cfg/.../tag_criteria.json + Table created: tagged_products + + + + + products + (10 batches) + + + + + + + + file_tagged_products_for_operator_review + All tagged products to file that can be inspected by the operator + Processor: read from table into files + Source table: tagged_products + File(s): /tmp/capi_out/.../tagged_products_for_operator_review.csv + + + + + tagged_products + (no parallelism) + + + + + + + + read_tags + Load tags from file + Processor: read from files into a table + File(s): /tmp/capi_in/.../tags.csv + Table created: tags + + + + + + + + tag_totals + For each tag, look up products and perform left outer join with grouping and aggregation + Processor: left join with lookup table, group: true + Lookup index: idx_tagged_products_tag + Table created: tag_totals + + + + + tags + (10 batches) + + + + + idx_tagged_products_tag + (lookup) + + + + + + + + file_tag_totals + Statistics for each tag + Processor: read from table into files + Source table: tag_totals + File(s): /tmp/capi_out/.../tag_totals.tsv + + + + + tag_totals + (no parallelism) + +Perms 2, elapsed 0.000s, dist 1011.6 + diff --git a/doc/what.md b/doc/what.md index 6e171281..5816e846 100644 --- a/doc/what.md +++ b/doc/what.md @@ -15,7 +15,7 @@ Capillaries is a distributed data processing framework that: For example, this Capillaries [script](glossary.md#script) - [test/data/cfg/tag_and_denormalize/script.json](../test/data/cfg/tag_and_denormalize_quicktest/script.json) - for the [tag_and_denormalize integration test](../test/code/tag_and_denormalize/README.md) can be described with this [DAG](glossary.md#DAG) diagram (open it the SVG in a separate browser window to see node tooltips, they may be helpful): -![dot-lookup](dot-tag-and-denormalize.svg) +![viz-tag-and-denormalize](viz-tag-and-denormalize.svg) Workflow steps are discussed in detail below. diff --git a/docker-compose.yml b/docker-compose.yml index 87cda296..8deabaa3 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,29 +51,30 @@ services: fluentd-async: "true" tag: cassandra - cassandra2: - container_name: capillaries_cassandra2 - build: - context: . - dockerfile: ./test/docker/cassandra/Dockerfile - depends_on: - - fluentd - networks: - capinet: - ipv4_address: 10.5.0.12 - ports: # Map to different host ports to avoid collision - - 17000:7000 - - 17199:7199 - - 19042:9042 - - 17070:7070 - environment: - CASSANDRA_SEEDS: 10.5.0.11 - logging: - driver: fluentd - options: - fluentd-address: 10.5.0.7:24224 - fluentd-async: "true" - tag: cassandra + # Two cassandras bring my laptop to its knees, make it one + # cassandra2: + # container_name: capillaries_cassandra2 + # build: + # context: . + # dockerfile: ./test/docker/cassandra/Dockerfile + # depends_on: + # - fluentd + # networks: + # capinet: + # ipv4_address: 10.5.0.12 + # ports: # Map to different host ports to avoid collision + # - 17000:7000 + # - 17199:7199 + # - 19042:9042 + # - 17070:7070 + # environment: + # CASSANDRA_SEEDS: 10.5.0.11 + # logging: + # driver: fluentd + # options: + # fluentd-address: 10.5.0.7:24224 + # fluentd-async: "true" + # tag: cassandra prometheus: container_name: capillaries_prometheus diff --git a/go.mod b/go.mod index 93547e9f..98dfd1bf 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,10 @@ require ( golang.org/x/crypto v0.27.0 golang.org/x/text v0.18.0 gopkg.in/inf.v0 v0.9.1 + gopkg.in/yaml.v3 v3.0.1 ) -require gopkg.in/yaml.v3 v3.0.1 // indirect +require github.com/kr/pretty v0.3.1 // indirect require ( github.com/apache/thrift v0.19.0 // indirect diff --git a/go.sum b/go.sum index bd3287bc..b39e05b5 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,7 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -72,8 +73,9 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -83,6 +85,7 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -90,6 +93,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= diff --git a/pkg/api/capigraph.go b/pkg/api/capigraph.go new file mode 100644 index 00000000..f17986d7 --- /dev/null +++ b/pkg/api/capigraph.go @@ -0,0 +1,390 @@ +package api + +import ( + "fmt" + "sort" + "strings" + + "github.com/capillariesio/capillaries/pkg/capigraph" + "github.com/capillariesio/capillaries/pkg/custom/py_calc" + "github.com/capillariesio/capillaries/pkg/custom/tag_and_denormalize" + "github.com/capillariesio/capillaries/pkg/sc" + "github.com/capillariesio/capillaries/pkg/wfmodel" +) + +const CapillariesIcons100x100 = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + +func NodeBatchStatusToCapigraphColor(status wfmodel.NodeBatchStatusType) int32 { + switch status { + case wfmodel.NodeBatchNone: + return 0 // not colored, white + case wfmodel.NodeBatchStart: + return 0x0000FF // blue + case wfmodel.NodeBatchSuccess: + return 0x008000 // darkgreen + case wfmodel.NodeBatchFail: + return 0xFF0000 // red + case wfmodel.NodeBatchRunStopReceived: + return 0xFF8C00 // darkorange + default: + return 0x2F4F4F // darkslategray + } +} + +func shortenFileName(s string) string { + parts := strings.Split(s, "/") + sb := strings.Builder{} + for i, s := range parts { + if i < 3 || i == len(parts)-1 { + if i > 0 { + sb.WriteString("/") + } + sb.WriteString(s) + } else if i == 3 { + sb.WriteString("/...") + } + } + return sb.String() +} + +func nodeTypeDescription(node *sc.ScriptNodeDef) string { + switch node.Type { + case sc.NodeTypeFileTable: + return fmt.Sprintf( + "Processor: read from files into a table\n"+ + "File(s): %s\n"+ + "Table created: %s", + shortenFileName(node.FileReader.SrcFileUrls[0]), + node.TableCreator.Name) + case sc.NodeTypeTableTable: + return fmt.Sprintf( + "Processor: write from table to table\n"+ + "Source table: %s\n"+ + "Table created: %s", + node.TableReader.TableName, + node.TableCreator.Name) + case sc.NodeTypeTableLookupTable: + return fmt.Sprintf( + "Processor: %s join with lookup table, group: %t\n"+ + "Lookup index: %s\n"+ + "Table created: %s", + node.Lookup.LookupJoin, + node.Lookup.IsGroup, + node.Lookup.IndexName, + node.TableCreator.Name) + case sc.NodeTypeTableFile: + return fmt.Sprintf( + "Processor: read from table into files\n"+ + "Source table: %s\n"+ + "File(s): %s", + node.TableReader.TableName, + shortenFileName(node.FileCreator.UrlTemplate)) + case sc.NodeTypeTableCustomTfmTable: + switch node.CustomProcessorType { + case py_calc.ProcessorPyCalcName: + return fmt.Sprintf( + "Processor: apply Python calculations\n"+ + "Python file(s): %s\n"+ + "Table created: %s", + shortenFileName(node.CustomProcessor.(*py_calc.PyCalcProcessorDef).PythonUrls[0]), + node.TableCreator.Name) + case tag_and_denormalize.ProcessorTagAndDenormalizeName: + tagCriteriaUrl := shortenFileName(node.CustomProcessor.(*tag_and_denormalize.TagAndDenormalizeProcessorDef).RawTagCriteriaUrl) + if len(tagCriteriaUrl) == 0 { + tagCriteriaUrl = "(inline)" + } + return fmt.Sprintf( + "Processor: tag and denormalize\n"+ + "Tag criteria URL: %s\n"+ + "Table created: %s", + tagCriteriaUrl, + node.TableCreator.Name) + default: + return "Custom processor: unknown" + } + case sc.NodeTypeDistinctTable: + distinctIdxName, _, err := node.TableCreator.GetSingleUniqueIndexDef() + if err != nil { + distinctIdxName = "unknown" + } + + return fmt.Sprintf( + "Processor: select distinct rows\n"+ + "Unique index: %s\n"+ + "Table created: %s", + distinctIdxName, + node.TableCreator.Name) + default: + return "Unknown processor type: " + string(node.Type) + } +} + +func nodeTypeIcon(node *sc.ScriptNodeDef) string { + switch node.Type { + case sc.NodeTypeFileTable: + return "icon-database-table-read" + case sc.NodeTypeTableTable: + return "icon-database-table-copy" + case sc.NodeTypeTableLookupTable: + return "icon-database-table-join" + case sc.NodeTypeTableFile: + if node.FileCreator.CreatorFileType == sc.CreatorFileTypeCsv { + return "icon-csv" + } else if node.FileCreator.CreatorFileType == sc.CreatorFileTypeParquet { + return "icon-parquet" + } else { + return "" + } + case sc.NodeTypeTableCustomTfmTable: + switch node.CustomProcessorType { + case py_calc.ProcessorPyCalcName: + return "icon-database-table-py" + case tag_and_denormalize.ProcessorTagAndDenormalizeName: + return "icon-database-table-tag" + default: + return "" + } + case sc.NodeTypeDistinctTable: + return "icon-database-table-distinct" + default: + + return "" + } +} + +func GetCapigraphDiagram(scriptDef *sc.ScriptDef, showIdx bool, showFields bool, useRootPalette bool, nodeStringColorMap map[string]int32) string { + nodeDefs := make([]capigraph.NodeDef, len(scriptDef.ScriptNodes)+1) + nodeDefs[0] = capigraph.NodeDef{0, "", capigraph.EdgeDef{}, []capigraph.EdgeDef{}, "", 0, false} + nodeNameMap := map[string]int16{} + + // Populate nodes. Before that, sort node names, otherwise they may appear on the diagram in random order (when best distances are close) + nodeNames := make([]string, len(scriptDef.ScriptNodes)) + nodeIdx := int16(0) + for nodeName := range scriptDef.ScriptNodes { + nodeNames[nodeIdx] = nodeName + nodeIdx++ + } + sort.Slice(nodeNames, func(i, j int) bool { return nodeNames[i] < nodeNames[j] }) + + nodeIdx = int16(1) + for _, nodeName := range nodeNames { + node := scriptDef.ScriptNodes[nodeName] + nodeNameMap[nodeName] = nodeIdx + color := int32(0) + if nodeStringColorMap != nil { + color = nodeStringColorMap[node.Name] + } + + // This is a root node marked as "manual". Do not show "manual" marker, + // it is redundant in this case and may be confusing + isReallyStartedManually := !node.HasFileReader() && node.StartPolicy == sc.NodeStartManual + if nodeStringColorMap != nil { + // Short desc + nodeDefs[nodeIdx] = capigraph.NodeDef{nodeIdx, fmt.Sprintf("\n%s\n", nodeName), capigraph.EdgeDef{}, []capigraph.EdgeDef{}, nodeTypeIcon(node), color, isReallyStartedManually} + } else { + // Full desc + nodeDefs[nodeIdx] = capigraph.NodeDef{nodeIdx, fmt.Sprintf("%s\n%s\n%s", nodeName, node.Desc, nodeTypeDescription(node)), capigraph.EdgeDef{}, []capigraph.EdgeDef{}, nodeTypeIcon(node), color, isReallyStartedManually} + } + nodeIdx++ + } + + // Populate direct parents and lookups + for _, node := range scriptDef.ScriptNodes { + nodeIdx := nodeNameMap[node.Name] + allUsedFields := sc.FieldRefs{} + if node.Type == sc.NodeTypeTableCustomTfmTable && node.CustomProcessorType == py_calc.ProcessorPyCalcName { + usedInPyExpressions := node.CustomProcessor.(*py_calc.PyCalcProcessorDef).GetUsedInTargetExpressionsFields() + allUsedFields.Append(*usedInPyExpressions) + } else if node.Type == sc.NodeTypeTableCustomTfmTable && node.CustomProcessorType == tag_and_denormalize.ProcessorTagAndDenormalizeName { + usedInTagExpressions := node.CustomProcessor.(*tag_and_denormalize.TagAndDenormalizeProcessorDef).GetUsedInTargetExpressionsFields() + allUsedFields.Append(*usedInTagExpressions) + } + if node.HasFileCreator() { + usedInAllTargetFileExpressions := node.FileCreator.GetFieldRefsUsedInAllTargetFileExpressions() + allUsedFields.Append(usedInAllTargetFileExpressions) + } else if node.HasTableCreator() { + usedInAllTargetTableExpressions := sc.GetFieldRefsUsedInAllTargetExpressions(node.TableCreator.Fields) + allUsedFields.Append(usedInAllTargetTableExpressions) + } + + if node.HasTableReader() { + parentNode := scriptDef.TableCreatorNodeMap[node.TableReader.TableName] + parentNodeIdx := nodeNameMap[parentNode.Name] + nodeDefs[nodeIdx].PriIn.SrcId = parentNodeIdx + sb := strings.Builder{} + if showIdx { + if node.TableReader.ExpectedBatchesTotal > 1 { + sb.WriteString(fmt.Sprintf("%s\n(%d batches)", node.TableReader.TableName, node.TableReader.ExpectedBatchesTotal)) + } else { + sb.WriteString(fmt.Sprintf("%s\n(no parallelism)", node.TableReader.TableName)) + } + } + if showFields { + if showIdx { + sb.WriteString("\n") + } + sort.Slice(allUsedFields, func(i, j int) bool { return allUsedFields[i].FieldName < allUsedFields[j].FieldName }) + for i := 0; i < len(allUsedFields); i++ { + if allUsedFields[i].TableName == sc.ReaderAlias { + if sb.Len() > 0 { + sb.WriteString("\n") + } + sb.WriteString(allUsedFields[i].FieldName) + } + } + } + nodeDefs[nodeIdx].PriIn.Text = sb.String() + } + if node.HasLookup() { + lkpParentNode := scriptDef.IndexNodeMap[node.Lookup.IndexName] + lkpParentNodeIdx := nodeNameMap[lkpParentNode.Name] + + sb := strings.Builder{} + if showIdx { + sb.WriteString(fmt.Sprintf("%s\n(lookup)", node.Lookup.IndexName)) + } + if showFields { + if showIdx { + sb.WriteString("\n") + } + for i := 0; i < len(allUsedFields); i++ { + if allUsedFields[i].TableName == sc.LookupAlias { + if sb.Len() > 0 { + sb.WriteString("\n") + } + sb.WriteString(allUsedFields[i].FieldName) + + } + } + } + nodeDefs[nodeIdx].SecIn = append(nodeDefs[nodeIdx].SecIn, capigraph.EdgeDef{lkpParentNodeIdx, sb.String()}) + } + } + + nodeFo := capigraph.FontOptions{capigraph.FontTypefaceVerdana, capigraph.FontWeightNormal, 20, 0.3} + edgeFo := capigraph.FontOptions{capigraph.FontTypefaceArial, capigraph.FontWeightNormal, 18, 0.3} + edgeOptions := capigraph.EdgeOptions{2.0} + palette := capigraph.DefaultPalette() + if nodeStringColorMap != nil || !useRootPalette { + palette = nil + } + svg, _, _, _, _, errOpt := capigraph.DrawOptimized(nodeDefs, nodeFo, edgeFo, edgeOptions, CapillariesIcons100x100, "", palette) + if errOpt != nil { + var errUnopt error + svg, _, errUnopt = capigraph.DrawUnoptimized(nodeDefs, nodeFo, edgeFo, edgeOptions, CapillariesIcons100x100, "", palette) + if errUnopt != nil { + svg = fmt.Sprintf(` + +%s +%s +`, errOpt.Error(), errUnopt.Error()) + } + } + + return svg + +} diff --git a/pkg/api/dot.go b/pkg/api/dot.go new file mode 100644 index 00000000..ca5bf91a --- /dev/null +++ b/pkg/api/dot.go @@ -0,0 +1,176 @@ +package api + +import ( + "fmt" + "strings" + + "github.com/capillariesio/capillaries/pkg/sc" + "github.com/capillariesio/capillaries/pkg/wfmodel" +) + +type DiagramType string + +const ( + DiagramIndexes DiagramType = "indexes" + DiagramFields DiagramType = "fields" + DiagramRunStatus DiagramType = "run_status" +) + +func NodeBatchStatusToDotColor(status wfmodel.NodeBatchStatusType) string { + switch status { + case wfmodel.NodeBatchNone: + return "white" + case wfmodel.NodeBatchStart: + return "lightblue" + case wfmodel.NodeBatchSuccess: + return "green" + case wfmodel.NodeBatchFail: + return "red" + case wfmodel.NodeBatchRunStopReceived: + return "orangered" + default: + return "cyan" + } +} + +func drawFileReader(node *sc.ScriptNodeDef, showFields bool, arrowFontSize int, recordFontSize int) string { + var b strings.Builder + arrowLabelBuilder := strings.Builder{} + if showFields { + for colName := range node.FileReader.Columns { + arrowLabelBuilder.WriteString(colName) + arrowLabelBuilder.WriteString("\\l") + } + } + fileNames := make([]string, len(node.FileReader.SrcFileUrls)) + copy(fileNames, node.FileReader.SrcFileUrls) + + b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=dotted, fontsize=\"%d\", label=\"%s\"];\n", node.FileReader.SrcFileUrls[0], node.GetTargetName(), arrowFontSize, arrowLabelBuilder.String())) + b.WriteString(fmt.Sprintf("\"%s\" [shape=folder, fontsize=\"%d\", label=\"%s\", tooltip=\"Source data file(s)\"];\n", node.FileReader.SrcFileUrls[0], recordFontSize, strings.Join(fileNames, "\\n"))) + return b.String() +} + +func drawFileCreator(node *sc.ScriptNodeDef, showFields bool, arrowFontSize int, recordFontSize int, allUsedFields sc.FieldRefs, penWidth string, fillColor string, urlEscaper *strings.Replacer, inSrcArrowLabel string) string { + var b strings.Builder + b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=solid, fontsize=\"%d\", label=\"%s\"];\n", node.TableReader.TableName, node.Name, arrowFontSize, inSrcArrowLabel)) + + // Node (file) + b.WriteString(fmt.Sprintf("\"%s\" [shape=record, penwidth=\"%s\", fontsize=\"%d\", fillcolor=\"%s\", style=\"filled\", label=\"{%s|creates file:\\n%s}\", tooltip=\"%s\"];\n", node.Name, penWidth, recordFontSize, fillColor, node.Name, urlEscaper.Replace(node.FileCreator.UrlTemplate), node.Desc)) + + // Out (file) + arrowLabelBuilder := strings.Builder{} + if showFields { + for i := 0; i < len(allUsedFields); i++ { + arrowLabelBuilder.WriteString(allUsedFields[i].FieldName) + arrowLabelBuilder.WriteString("\\l") + } + } + + b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=dotted, fontsize=\"%d\", label=\"%s\"];\n", node.Name, node.FileCreator.UrlTemplate, arrowFontSize, arrowLabelBuilder.String())) + b.WriteString(fmt.Sprintf("\"%s\" [shape=note, fontsize=\"%d\", label=\"%s\", tooltip=\"Target data file(s)\"];\n", node.FileCreator.UrlTemplate, recordFontSize, node.FileCreator.UrlTemplate)) + return b.String() +} + +func drawTableReader(node *sc.ScriptNodeDef, showIdx bool, showFields bool, arrowFontSize int, recordFontSize int, allUsedFields sc.FieldRefs, penWidth string, fillColor string, urlEscaper *strings.Replacer) string { + var b strings.Builder + sb := strings.Builder{} + if showIdx { + if node.TableReader.ExpectedBatchesTotal > 1 { + sb.WriteString(fmt.Sprintf("%s (%d batches)", node.TableReader.TableName, node.TableReader.ExpectedBatchesTotal)) + } else { + sb.WriteString(fmt.Sprintf("%s (no parallelism)", node.TableReader.TableName)) + } + } + if showFields { + if showIdx { + sb.WriteString("\\l") + } + for i := 0; i < len(allUsedFields); i++ { + if allUsedFields[i].TableName == sc.ReaderAlias { + sb.WriteString(allUsedFields[i].FieldName) + sb.WriteString("\\l") + } + } + } + + if node.HasFileCreator() { + b.WriteString(drawFileCreator(node, showFields, arrowFontSize, recordFontSize, allUsedFields, penWidth, fillColor, urlEscaper, sb.String())) + } else { + b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=solid, fontsize=\"%d\", label=\"%s\"];\n", node.TableReader.TableName, node.GetTargetName(), arrowFontSize, sb.String())) + } + + if node.HasLookup() { + inLkpArrowLabel := fmt.Sprintf("%s (lookup)", node.Lookup.IndexName) + if showFields { + inLkpArrowLabelBuilder := strings.Builder{} + for i := 0; i < len(allUsedFields); i++ { + if allUsedFields[i].TableName == sc.LookupAlias { + inLkpArrowLabelBuilder.WriteString(allUsedFields[i].FieldName) + inLkpArrowLabelBuilder.WriteString("\\l") + } + } + inLkpArrowLabel = inLkpArrowLabelBuilder.String() + } + // In (lookup) + b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=dashed, fontsize=\"%d\", label=\"%s\"];\n", node.Lookup.TableCreator.Name, node.GetTargetName(), arrowFontSize, inLkpArrowLabel)) + } + return b.String() +} + +func drawTableCreator(node *sc.ScriptNodeDef, recordFontSize int, penWidth string, fillColor string) string { + if node.HasLookup() { + return fmt.Sprintf("\"%s\" [shape=record, penwidth=\"%s\", fontsize=\"%d\", fillcolor=\"%s\", style=\"filled\", label=\"{%s|creates table:\\n%s|group:%t, join:%s}\", tooltip=\"%s\"];\n", + node.TableCreator.Name, penWidth, recordFontSize, fillColor, node.Name, node.TableCreator.Name, node.Lookup.IsGroup, node.Lookup.LookupJoin, node.Desc) + } + return fmt.Sprintf("\"%s\" [shape=record, penwidth=\"%s\", fontsize=\"%d\", fillcolor=\"%s\", style=\"filled\", label=\"{%s|creates table:\\n%s}\", tooltip=\"%s\"];\n", + node.TableCreator.Name, penWidth, recordFontSize, fillColor, node.Name, node.TableCreator.Name, node.Desc) +} + +// Used by Toolbelt and Webapi +func GetDotDiagram(scriptDef *sc.ScriptDef, showIdx bool, showFields bool, nodeColorMap map[string]string) string { + var b strings.Builder + + const recordFontSize int = 20 + const arrowFontSize int = 18 + + urlEscaper := strings.NewReplacer(`{`, `\{`, `}`, `\}`, `|`, `\|`) + b.WriteString("\ndigraph idx_fields {\nrankdir=\"TD\";\n node [fontname=\"Helvetica\"];\nedge [fontname=\"Helvetica\"];\ngraph [splines=true, pad=\"0.5\", ranksep=\"0.5\", nodesep=\"0.5\"];\n") + for _, node := range scriptDef.ScriptNodes { + penWidth := "1" + if node.StartPolicy == sc.NodeStartManual { + penWidth = "6" + } + fillColor := "white" + var ok bool + if nodeColorMap != nil { + if fillColor, ok = nodeColorMap[node.Name]; !ok { + fillColor = "white" // This run does not affect this node, or the node was not started + } + } + + if node.HasFileReader() { + b.WriteString(drawFileReader(node, showFields, arrowFontSize, recordFontSize)) + } + + allUsedFields := sc.FieldRefs{} + + if node.HasFileCreator() { + usedInAllTargetFileExpressions := node.FileCreator.GetFieldRefsUsedInAllTargetFileExpressions() + allUsedFields.Append(usedInAllTargetFileExpressions) + } else if node.HasTableCreator() { + usedInAllTargetTableExpressions := sc.GetFieldRefsUsedInAllTargetExpressions(node.TableCreator.Fields) + allUsedFields.Append(usedInAllTargetTableExpressions) + } + + if node.HasTableReader() { + b.WriteString(drawTableReader(node, showIdx, showFields, arrowFontSize, recordFontSize, allUsedFields, penWidth, fillColor, urlEscaper)) + } + + if node.HasTableCreator() { + b.WriteString(drawTableCreator(node, recordFontSize, penWidth, fillColor)) + } + } + b.WriteString("}\n") + + return b.String() +} diff --git a/pkg/api/run.go b/pkg/api/run.go index 1c8bd8c5..9ecc7d42 100644 --- a/pkg/api/run.go +++ b/pkg/api/run.go @@ -117,8 +117,8 @@ func StartRun(envConfig *env.EnvConfig, logger *l.CapiLogger, amqpChannel *amqp. Ts: time.Now().UnixMilli(), MessageType: wfmodel.MessageTypeDataBatch, Payload: wfmodel.MessagePayloadDataBatch{ - ScriptURI: scriptFilePath, - ScriptParamsURI: paramsFilePath, + ScriptURL: scriptFilePath, + ScriptParamsURL: paramsFilePath, DataKeyspace: keyspace, RunId: runId, TargetNodeName: affectedNodeName, @@ -235,8 +235,8 @@ func RunNode(envConfig *env.EnvConfig, logger *l.CapiLogger, nodeName string, ru batchStartTs := time.Now() logger.Info("BatchStarted: [%d,%d]...", intervals[i][0], intervals[i][1]) dataBatchInfo := wfmodel.MessagePayloadDataBatch{ - ScriptURI: scriptFilePath, - ScriptParamsURI: paramsFilePath, + ScriptURL: scriptFilePath, + ScriptParamsURL: paramsFilePath, DataKeyspace: keyspace, RunId: runId, TargetNodeName: nodeName, diff --git a/pkg/capigraph/fonts.go b/pkg/capigraph/fonts.go new file mode 100644 index 00000000..47004355 --- /dev/null +++ b/pkg/capigraph/fonts.go @@ -0,0 +1,202 @@ +package capigraph + +type FontWeight int + +const ( + FontWeightBold FontWeight = iota + FontWeightNormal +) + +func FontWeightToString(w FontWeight) string { + switch w { + case FontWeightBold: + return "bold" + case FontWeightNormal: + return "normal" + default: + return "normal" + } +} + +type FontTypeface int + +const ( + FontTypefaceArial FontTypeface = iota + FontTypefaceCourier + FontTypefaceVerdana +) + +func FontTypefaceToString(tf FontTypeface) string { + switch tf { + case FontTypefaceArial: + return "arial" + case FontTypefaceCourier: + return "courier" + case FontTypefaceVerdana: + return "verdana" + default: + return "courier" + } +} + +type FontOptions struct { + Typeface FontTypeface + Weight FontWeight + SizeInPixels float64 + Interval float64 +} + +// Generated by https://github.com/Evgenus/js-server-text-width +// 0x0: Basic Latin, Latin-1 Supplement, Latin Extended-A, Latin Extended-B +// 0x370: Greek and Coptic, Cyrillic +// Normal (400) and bold (700), Arial, Courier, Verdana font size 100px +var sizeMap = map[FontTypeface]map[FontWeight]map[int][]int{ + FontTypefaceArial: { + FontWeightNormal: { + // "Arial|100px|400|0" + 0: []int{75, 75, 75, 75, 75, 75, 75, 75, 75, 28, 28, 28, 28, 28, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 28, 28, 35, 56, 56, 89, 67, 19, 33, 33, 39, 58, 28, 33, 28, 28, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 28, 28, 58, 58, 58, 56, 102, 67, 67, 72, 72, 67, 61, 78, 72, 28, 50, 67, 56, 83, 72, 78, 67, 78, 72, 67, 61, 72, 67, 94, 67, 67, 61, 28, 28, 28, 47, 56, 33, 56, 56, 50, 56, 56, 28, 56, 56, 22, 22, 50, 22, 83, 56, 56, 56, 56, 33, 50, 28, 56, 50, 72, 50, 50, 50, 33, 26, 33, 58, 50, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 28, 33, 56, 56, 56, 56, 26, 56, 33, 74, 37, 56, 58, 0, 74, 55, 40, 55, 33, 33, 33, 58, 54, 33, 33, 33, 37, 56, 83, 83, 83, 61, 67, 67, 67, 67, 67, 67, 100, 72, 67, 67, 67, 67, 28, 28, 28, 28, 72, 72, 78, 78, 78, 78, 78, 58, 78, 72, 72, 72, 72, 67, 67, 61, 56, 56, 56, 56, 56, 56, 89, 50, 56, 56, 56, 56, 28, 28, 28, 28, 56, 56, 56, 56, 56, 56, 56, 55, 61, 56, 56, 56, 56, 50, 56, 50, 67, 56, 67, 56, 67, 56, 72, 50, 72, 50, 72, 50, 72, 50, 72, 61, 72, 56, 67, 56, 67, 56, 67, 56, 67, 56, 67, 56, 78, 56, 78, 56, 78, 56, 78, 56, 72, 56, 72, 56, 28, 28, 28, 28, 28, 28, 28, 22, 28, 28, 73, 44, 50, 22, 67, 50, 50, 56, 22, 56, 22, 56, 29, 56, 33, 56, 22, 72, 56, 72, 56, 72, 56, 60, 72, 56, 78, 56, 78, 56, 78, 56, 100, 94, 72, 33, 72, 33, 72, 33, 67, 50, 67, 50, 67, 50, 67, 50, 61, 28, 61, 38, 61, 28, 72, 56, 72, 56, 72, 56, 72, 56, 72, 56, 72, 56, 94, 72, 67, 50, 67, 61, 50, 61, 50, 61, 50, 22, 56, 76, 66, 56, 66, 56, 72, 72, 50, 72, 81, 66, 56, 56, 67, 75, 60, 61, 56, 78, 62, 88, 22, 28, 67, 50, 22, 50, 89, 72, 56, 78, 86, 66, 87, 67, 75, 56, 67, 67, 50, 62, 38, 28, 61, 28, 61, 85, 67, 75, 72, 77, 50, 61, 50, 61, 61, 54, 54, 56, 56, 46, 49, 56, 26, 41, 58, 28, 133, 122, 105, 106, 83, 45, 122, 94, 77, 67, 56, 28, 22, 78, 56, 72, 56, 72, 56, 72, 56, 72, 56, 72, 56, 56, 67, 56, 67, 56, 100, 89, 78, 56, 78, 56, 67, 50, 78, 56, 78, 56, 61, 54, 22, 133, 122, 105, 78, 56, 103, 62, 72, 56, 67, 56, 100, 89, 78, 61, 67, 56, 67, 56, 67, 56, 67, 56, 28, 28, 28, 28, 78, 56, 78, 56, 72, 33, 72, 33, 72, 56, 72, 56, 67, 50, 61, 28, 54, 44, 72, 56, 71, 68, 60, 57, 61, 50, 67, 56, 67, 56, 78, 56, 78, 56, 78, 56, 78, 56, 67, 50, 35, 68, 37, 22, 89, 89, 67, 72, 50, 56, 61, 50, 50, 58, 45, 67, 72, 67, 67, 56, 50, 22, 74, 56, 72, 33, 67, 50}, // "Arial|100px|400|0" + // "Arial|100px|400|370" + 0x370: []int{58, 48, 61, 46, 33, 33, 72, 58, 75, 75, 33, 50, 50, 50, 28, 50, 75, 75, 75, 75, 33, 33, 67, 28, 78, 84, 38, 75, 77, 75, 86, 75, 22, 67, 67, 55, 67, 67, 61, 72, 78, 28, 67, 67, 83, 72, 65, 78, 72, 67, 75, 62, 61, 67, 80, 67, 84, 75, 28, 67, 58, 45, 56, 22, 55, 58, 58, 50, 56, 45, 44, 56, 56, 22, 50, 50, 58, 50, 45, 56, 69, 57, 48, 62, 40, 55, 65, 52, 71, 78, 22, 55, 56, 55, 78, 67, 58, 55, 77, 96, 77, 56, 78, 60, 78, 56, 72, 50, 61, 40, 62, 53, 76, 58, 89, 83, 67, 56, 67, 50, 67, 67, 61, 60, 74, 55, 46, 41, 60, 57, 50, 22, 78, 44, 44, 67, 56, 72, 83, 69, 57, 72, 72, 72, 67, 67, 86, 54, 72, 67, 28, 28, 50, 106, 101, 85, 58, 72, 64, 72, 67, 66, 67, 54, 68, 67, 92, 60, 72, 72, 58, 66, 83, 72, 78, 72, 67, 72, 61, 64, 76, 67, 74, 67, 92, 94, 79, 89, 66, 72, 101, 72, 56, 57, 53, 36, 58, 56, 67, 46, 56, 56, 44, 58, 69, 55, 56, 54, 56, 50, 46, 50, 82, 50, 57, 52, 80, 82, 63, 72, 52, 51, 75, 54, 56, 56, 56, 36, 51, 50, 22, 28, 22, 91, 81, 56, 44, 56, 50, 55, 134, 62, 78, 61, 95, 71, 67, 50, 90, 70, 83, 69, 105, 87, 60, 46, 80, 69, 78, 56, 80, 63, 80, 63, 107, 90, 83, 61, 119, 85, 134, 62, 72, 50, 50, 0, 0, 0, 0, 0, 0, 0, 72, 56, 66, 52, 67, 56, 49, 41, 54, 36, 67, 55, 92, 67, 60, 46, 58, 44, 58, 44, 58, 44, 74, 54, 72, 55, 88, 65, 114, 87, 75, 52, 72, 50, 61, 46, 56, 50, 56, 50, 67, 50, 93, 69, 67, 52, 67, 52, 67, 56, 86, 67, 86, 67, 28, 92, 67, 67, 55, 66, 58, 72, 55, 72, 55, 67, 52, 83, 69, 22, 67, 56, 67, 56, 100, 89, 67, 56, 75, 56, 75, 56, 92, 67, 60, 46, 60, 54, 72, 56, 72, 56, 78, 56, 78, 56, 78, 56, 72, 51, 64, 50, 64, 50, 64, 50, 67, 52, 54, 36, 89, 72, 54, 36, 67, 50, 67, 50}, // "Arial|100px|400|370" + }, + FontWeightBold: { + // "Arial|100px|700|0" + 0: []int{75, 75, 75, 75, 75, 75, 75, 75, 75, 28, 28, 28, 28, 28, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 28, 33, 47, 56, 56, 89, 72, 24, 33, 33, 39, 58, 28, 33, 28, 28, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 33, 33, 58, 58, 58, 61, 98, 72, 72, 72, 72, 67, 61, 78, 72, 28, 56, 72, 61, 83, 72, 78, 67, 78, 72, 67, 61, 72, 67, 94, 67, 67, 61, 33, 28, 33, 58, 56, 33, 56, 61, 56, 61, 56, 33, 61, 61, 28, 28, 56, 28, 89, 61, 61, 61, 61, 39, 56, 33, 61, 56, 78, 56, 56, 50, 39, 28, 39, 58, 50, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 28, 33, 56, 56, 56, 56, 28, 56, 33, 74, 37, 56, 58, 0, 74, 55, 40, 55, 33, 33, 33, 58, 56, 33, 33, 33, 37, 56, 83, 83, 83, 61, 72, 72, 72, 72, 72, 72, 100, 72, 67, 67, 67, 67, 28, 28, 28, 28, 72, 72, 78, 78, 78, 78, 78, 58, 78, 72, 72, 72, 72, 67, 67, 61, 56, 56, 56, 56, 56, 56, 89, 56, 56, 56, 56, 56, 28, 28, 28, 28, 61, 61, 61, 61, 61, 61, 61, 55, 61, 61, 61, 61, 61, 56, 61, 56, 72, 56, 72, 56, 72, 56, 72, 56, 72, 56, 72, 56, 72, 56, 72, 72, 72, 61, 67, 56, 67, 56, 67, 56, 67, 56, 67, 56, 78, 61, 78, 61, 78, 61, 78, 61, 72, 61, 72, 61, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 78, 56, 56, 28, 72, 56, 56, 61, 28, 61, 28, 61, 39, 61, 48, 61, 28, 72, 61, 72, 61, 72, 61, 71, 72, 61, 78, 61, 78, 61, 78, 61, 100, 94, 72, 39, 72, 39, 72, 39, 67, 56, 67, 56, 67, 56, 67, 56, 61, 33, 61, 48, 61, 33, 72, 61, 72, 61, 72, 61, 72, 61, 72, 61, 72, 61, 94, 78, 67, 56, 67, 61, 50, 61, 50, 61, 50, 28, 61, 84, 72, 61, 72, 61, 72, 72, 56, 72, 84, 72, 61, 61, 67, 73, 63, 61, 56, 78, 64, 93, 28, 28, 72, 56, 28, 56, 99, 72, 61, 78, 85, 71, 89, 74, 78, 61, 67, 67, 56, 60, 36, 33, 61, 33, 61, 83, 72, 80, 72, 77, 56, 61, 50, 61, 61, 53, 53, 56, 56, 50, 58, 61, 28, 50, 58, 33, 133, 122, 111, 117, 89, 56, 128, 100, 89, 72, 56, 28, 28, 78, 61, 72, 61, 72, 61, 72, 61, 72, 61, 72, 61, 56, 72, 56, 72, 56, 100, 89, 78, 61, 78, 61, 72, 56, 78, 61, 78, 61, 61, 53, 28, 133, 122, 111, 78, 61, 103, 67, 72, 61, 72, 56, 100, 89, 78, 61, 72, 56, 72, 56, 67, 56, 67, 56, 28, 28, 28, 28, 78, 61, 78, 61, 72, 39, 72, 39, 72, 61, 72, 61, 67, 56, 61, 33, 58, 52, 72, 61, 70, 84, 63, 63, 61, 50, 72, 56, 67, 56, 78, 61, 78, 61, 78, 61, 78, 61, 67, 56, 50, 83, 51, 28, 95, 95, 72, 72, 56, 61, 61, 56, 50, 65, 48, 72, 72, 67, 67, 56, 56, 28, 77, 61, 72, 39, 67, 56}, + // "Arial|100px|700|370" + 0x370: []int{54, 46, 61, 58, 33, 33, 72, 63, 75, 75, 33, 56, 56, 56, 33, 56, 75, 75, 75, 75, 33, 46, 72, 33, 85, 91, 47, 75, 82, 75, 93, 84, 28, 72, 72, 60, 72, 67, 61, 72, 78, 28, 72, 67, 83, 72, 64, 78, 72, 67, 75, 60, 61, 67, 82, 67, 81, 80, 28, 67, 61, 45, 61, 28, 58, 61, 61, 56, 61, 47, 46, 61, 54, 28, 56, 56, 61, 56, 45, 61, 77, 62, 52, 68, 45, 58, 72, 58, 75, 84, 28, 58, 61, 58, 84, 72, 61, 58, 77, 100, 77, 75, 84, 68, 78, 61, 72, 56, 61, 45, 75, 53, 80, 61, 99, 89, 70, 61, 70, 60, 67, 67, 64, 60, 73, 58, 51, 44, 68, 62, 56, 28, 78, 48, 48, 67, 61, 72, 83, 74, 62, 72, 72, 72, 67, 67, 89, 57, 71, 67, 28, 28, 56, 109, 106, 88, 61, 72, 62, 72, 72, 72, 72, 57, 71, 67, 90, 63, 72, 72, 61, 70, 83, 72, 78, 72, 67, 72, 61, 62, 85, 67, 73, 70, 100, 102, 87, 98, 72, 71, 103, 72, 56, 62, 61, 42, 63, 56, 71, 50, 61, 61, 50, 64, 74, 60, 61, 60, 61, 56, 49, 56, 88, 56, 61, 58, 83, 84, 73, 85, 61, 55, 85, 58, 56, 56, 61, 42, 55, 56, 28, 28, 28, 97, 91, 61, 50, 61, 56, 60, 128, 78, 87, 70, 98, 80, 67, 56, 93, 80, 81, 75, 108, 98, 63, 50, 81, 75, 78, 61, 81, 65, 81, 65, 112, 97, 81, 64, 128, 90, 128, 78, 72, 56, 58, 0, 0, 0, 0, 0, 0, 0, 72, 61, 72, 61, 67, 61, 49, 45, 57, 42, 70, 60, 90, 71, 63, 50, 61, 50, 61, 50, 61, 50, 76, 62, 72, 60, 88, 71, 113, 92, 72, 58, 72, 56, 61, 49, 56, 56, 56, 56, 67, 56, 85, 69, 70, 58, 70, 58, 70, 61, 86, 68, 86, 68, 28, 90, 71, 70, 60, 70, 64, 72, 60, 72, 60, 70, 58, 83, 74, 28, 72, 56, 72, 56, 100, 89, 67, 56, 73, 56, 73, 56, 90, 71, 63, 50, 63, 53, 72, 61, 72, 61, 78, 61, 78, 61, 78, 61, 71, 55, 62, 56, 62, 56, 62, 56, 70, 58, 57, 42, 98, 85, 57, 42, 67, 56, 67, 56}, + }, + }, + FontTypefaceCourier: { + FontWeightNormal: { + // "Courier|100px|400|0" + 0: []int{60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 50, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 0, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 0, 0, 0, 0, 0, 0, 0, 0, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 28, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60}, + // "Courier|100px|400|370" + 0x370: []int{60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 0, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60}, // "Courier|100px|400|370" + }, + FontWeightBold: { + // "Courier|100px|700|0" + 0: []int{60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 50, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 0, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 0, 0, 0, 0, 0, 0, 0, 0, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 33, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60}, + // "Courier|100px|700|370" + 0x370: []int{60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 0, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60}, + }, + }, + FontTypefaceVerdana: { + FontWeightNormal: { + // "Verdana|100px|400|0" + 0: []int{100, 100, 100, 100, 100, 100, 100, 100, 100, 35, 35, 35, 35, 35, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 35, 39, 46, 82, 64, 108, 73, 27, 45, 45, 64, 82, 36, 45, 36, 45, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 45, 45, 82, 82, 82, 55, 100, 68, 69, 70, 77, 63, 57, 78, 75, 42, 45, 69, 56, 84, 75, 79, 60, 79, 70, 68, 62, 73, 68, 99, 69, 62, 69, 45, 45, 45, 82, 64, 64, 60, 62, 52, 62, 60, 35, 62, 63, 27, 34, 59, 27, 97, 63, 61, 62, 62, 43, 52, 39, 63, 59, 82, 59, 59, 53, 63, 45, 63, 82, 50, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 35, 39, 64, 64, 64, 64, 45, 64, 64, 100, 55, 64, 82, 0, 100, 64, 54, 82, 54, 54, 64, 64, 64, 36, 64, 54, 55, 64, 100, 100, 100, 55, 68, 68, 68, 68, 68, 68, 98, 70, 63, 63, 63, 63, 42, 42, 42, 42, 78, 75, 79, 79, 79, 79, 79, 82, 79, 73, 73, 73, 73, 62, 61, 62, 60, 60, 60, 60, 60, 60, 96, 52, 60, 60, 60, 60, 27, 27, 27, 27, 61, 63, 61, 61, 61, 61, 61, 82, 61, 63, 63, 63, 63, 59, 62, 59, 68, 60, 68, 60, 68, 60, 70, 52, 70, 52, 70, 52, 70, 52, 77, 65, 78, 62, 63, 60, 63, 60, 63, 60, 63, 60, 63, 60, 78, 62, 78, 62, 78, 62, 78, 62, 75, 63, 75, 63, 42, 27, 42, 27, 42, 27, 42, 27, 42, 27, 87, 61, 45, 34, 69, 59, 59, 56, 27, 56, 27, 56, 30, 56, 46, 56, 28, 75, 63, 75, 63, 75, 63, 73, 75, 63, 79, 61, 79, 61, 79, 61, 107, 98, 70, 43, 70, 43, 70, 43, 68, 52, 68, 52, 68, 52, 68, 52, 62, 39, 62, 39, 62, 39, 73, 63, 73, 63, 73, 63, 73, 63, 73, 63, 73, 63, 99, 82, 62, 59, 62, 69, 53, 69, 53, 69, 53, 30, 50, 76, 57, 50, 57, 50, 67, 67, 44, 72, 81, 57, 50, 47, 61, 75, 50, 56, 64, 72, 72, 77, 25, 33, 72, 50, 28, 48, 82, 72, 50, 72, 81, 61, 92, 69, 65, 50, 56, 56, 39, 58, 34, 28, 61, 28, 61, 76, 66, 74, 72, 78, 50, 61, 44, 54, 54, 44, 44, 50, 50, 44, 42, 50, 20, 28, 25, 33, 133, 117, 94, 100, 89, 56, 111, 100, 78, 132, 124, 106, 91, 142, 124, 137, 127, 72, 50, 73, 63, 137, 127, 73, 63, 44, 72, 44, 72, 44, 89, 67, 72, 50, 141, 126, 133, 123, 72, 50, 72, 50, 54, 44, 98, 133, 117, 94, 78, 62, 95, 56, 75, 63, 68, 60, 98, 96, 79, 61, 72, 44, 72, 44, 61, 44, 61, 44, 33, 28, 33, 28, 72, 50, 72, 50, 67, 33, 67, 33, 72, 50, 72, 50, 68, 52, 62, 39, 56, 40, 139, 127, 65, 50, 60, 50, 61, 44, 72, 44, 61, 44, 72, 50, 72, 50, 72, 50, 72, 50, 72, 50, 28, 50, 32, 34, 77, 77, 72, 67, 50, 61, 61, 39, 44, 53, 40, 67, 72, 73, 61, 44, 39, 28, 70, 50, 67, 33, 72, 50}, + // "Verdana|100px|400|370" + 0x370: []int{55, 41, 61, 44, 33, 33, 72, 54, 100, 100, 33, 44, 44, 44, 45, 39, 100, 100, 100, 100, 64, 64, 68, 45, 75, 87, 54, 100, 88, 100, 75, 91, 27, 68, 69, 57, 70, 63, 69, 75, 79, 42, 69, 69, 84, 75, 65, 79, 75, 60, 100, 67, 62, 62, 82, 69, 87, 82, 42, 62, 62, 51, 63, 27, 63, 62, 62, 59, 61, 51, 46, 63, 62, 27, 59, 59, 64, 59, 50, 61, 64, 63, 51, 63, 50, 63, 79, 59, 82, 81, 27, 63, 61, 63, 81, 66, 51, 50, 72, 89, 72, 53, 71, 56, 72, 50, 67, 42, 56, 45, 58, 45, 73, 55, 83, 78, 62, 52, 66, 44, 54, 54, 66, 58, 70, 51, 48, 39, 56, 51, 44, 28, 72, 40, 40, 56, 50, 67, 89, 63, 50, 67, 67, 67, 63, 63, 79, 57, 70, 68, 42, 42, 45, 112, 110, 82, 69, 75, 62, 75, 68, 69, 69, 57, 75, 63, 97, 62, 75, 75, 69, 73, 84, 75, 79, 75, 60, 70, 62, 62, 82, 69, 76, 71, 103, 104, 78, 92, 68, 70, 103, 71, 60, 61, 59, 47, 62, 60, 80, 52, 64, 64, 59, 62, 70, 64, 61, 64, 62, 53, 50, 59, 84, 59, 64, 61, 88, 89, 64, 79, 57, 55, 84, 60, 60, 60, 63, 47, 55, 52, 27, 27, 34, 91, 91, 63, 59, 64, 59, 64, 117, 63, 67, 54, 97, 68, 72, 59, 103, 83, 90, 69, 121, 94, 50, 40, 74, 63, 72, 50, 81, 59, 81, 59, 119, 104, 76, 57, 98, 81, 117, 63, 67, 44, 33, 0, 0, 0, 0, 64, 0, 0, 72, 54, 57, 47, 56, 50, 57, 47, 57, 47, 63, 52, 97, 80, 50, 40, 69, 59, 69, 59, 67, 49, 79, 57, 75, 64, 85, 62, 103, 79, 79, 62, 67, 44, 62, 50, 62, 59, 62, 59, 69, 59, 91, 75, 65, 50, 71, 61, 71, 63, 88, 69, 88, 69, 33, 161, 143, 67, 52, 68, 50, 72, 54, 72, 54, 65, 50, 89, 63, 28, 132, 124, 132, 124, 89, 67, 127, 123, 75, 60, 139, 123, 161, 143, 125, 116, 50, 44, 72, 54, 139, 128, 142, 124, 79, 61, 142, 124, 134, 118, 71, 50, 125, 123, 125, 123, 135, 124, 57, 47, 156, 143, 58, 41, 72, 50, 72, 50}, + }, + FontWeightBold: { + // "Verdana|100px|700|0" + 0: []int{100, 100, 100, 100, 100, 100, 100, 100, 100, 34, 34, 34, 34, 34, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 34, 40, 59, 87, 71, 127, 86, 33, 54, 54, 71, 87, 36, 48, 36, 69, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 40, 40, 87, 87, 87, 62, 96, 78, 76, 72, 83, 68, 65, 81, 84, 55, 56, 77, 64, 95, 85, 85, 73, 85, 78, 71, 68, 81, 76, 113, 76, 74, 69, 54, 69, 54, 87, 71, 71, 67, 70, 59, 70, 66, 42, 70, 71, 34, 40, 67, 34, 106, 71, 69, 70, 70, 50, 59, 46, 71, 65, 98, 67, 65, 60, 71, 54, 71, 87, 50, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 34, 40, 71, 71, 71, 71, 54, 71, 71, 96, 60, 85, 87, 0, 96, 71, 59, 87, 60, 60, 71, 72, 71, 36, 71, 60, 60, 85, 118, 118, 118, 62, 78, 78, 78, 78, 78, 78, 109, 72, 68, 68, 68, 68, 55, 55, 55, 55, 83, 85, 85, 85, 85, 85, 85, 87, 85, 81, 81, 81, 81, 74, 73, 71, 67, 67, 67, 67, 67, 67, 102, 59, 66, 66, 66, 66, 34, 34, 34, 34, 68, 71, 69, 69, 69, 69, 69, 87, 69, 71, 71, 71, 71, 65, 70, 65, 78, 67, 78, 67, 78, 67, 72, 59, 72, 59, 72, 59, 72, 59, 83, 88, 83, 70, 68, 66, 68, 66, 68, 66, 68, 66, 68, 66, 81, 70, 81, 70, 81, 70, 81, 70, 84, 71, 84, 71, 55, 34, 55, 34, 55, 34, 55, 34, 55, 34, 101, 73, 56, 40, 77, 67, 67, 64, 34, 64, 34, 64, 52, 64, 56, 64, 35, 85, 71, 85, 71, 85, 71, 83, 85, 71, 85, 69, 85, 69, 85, 69, 114, 107, 78, 50, 78, 50, 78, 50, 71, 59, 71, 59, 71, 59, 71, 59, 68, 46, 68, 47, 68, 46, 81, 71, 81, 71, 81, 71, 81, 71, 81, 71, 81, 71, 113, 98, 74, 65, 74, 69, 60, 69, 60, 69, 60, 34, 56, 75, 66, 56, 66, 56, 72, 72, 44, 72, 82, 66, 56, 52, 67, 81, 53, 61, 71, 78, 72, 78, 31, 39, 78, 56, 28, 49, 94, 72, 56, 78, 91, 69, 106, 77, 69, 56, 61, 56, 39, 65, 51, 33, 67, 33, 67, 85, 74, 80, 65, 79, 50, 67, 44, 58, 58, 46, 45, 50, 50, 38, 33, 60, 22, 32, 28, 33, 139, 117, 100, 117, 100, 61, 122, 106, 89, 149, 138, 126, 105, 156, 140, 152, 142, 72, 56, 81, 71, 152, 142, 81, 71, 44, 72, 50, 72, 50, 100, 72, 78, 50, 152, 141, 148, 138, 78, 50, 78, 50, 58, 46, 111, 139, 117, 100, 81, 70, 97, 65, 85, 71, 78, 67, 109, 102, 85, 69, 72, 50, 72, 50, 67, 44, 67, 44, 39, 28, 39, 28, 78, 50, 78, 50, 72, 44, 72, 44, 72, 56, 72, 56, 71, 59, 68, 46, 55, 37, 155, 142, 73, 56, 68, 50, 67, 44, 72, 50, 67, 44, 78, 50, 78, 50, 78, 50, 78, 50, 72, 50, 28, 56, 36, 40, 82, 82, 72, 72, 50, 67, 67, 39, 44, 53, 47, 67, 72, 72, 67, 44, 50, 33, 82, 56, 72, 44, 72, 50}, + // "Verdana|100px|700|370" + 0x370: []int{61, 48, 70, 52, 33, 33, 78, 58, 100, 100, 33, 44, 44, 44, 40, 50, 100, 100, 100, 100, 71, 71, 80, 40, 85, 100, 71, 100, 97, 100, 94, 97, 34, 78, 76, 64, 81, 68, 69, 84, 85, 55, 77, 78, 95, 85, 71, 85, 84, 73, 100, 68, 68, 74, 95, 76, 98, 84, 55, 74, 70, 58, 71, 34, 71, 70, 72, 65, 69, 58, 55, 71, 70, 34, 67, 65, 72, 65, 58, 69, 72, 70, 56, 73, 54, 71, 91, 64, 94, 89, 34, 71, 69, 71, 89, 78, 53, 60, 72, 92, 72, 62, 71, 58, 78, 50, 72, 42, 61, 55, 60, 46, 75, 55, 87, 83, 73, 59, 74, 44, 63, 59, 68, 61, 72, 52, 54, 45, 58, 50, 44, 33, 78, 42, 42, 61, 56, 72, 94, 68, 54, 72, 72, 72, 68, 68, 91, 64, 74, 71, 55, 55, 56, 122, 121, 94, 77, 85, 74, 84, 78, 76, 76, 64, 84, 68, 112, 71, 85, 85, 77, 85, 95, 84, 85, 84, 73, 72, 68, 74, 95, 76, 85, 79, 116, 118, 91, 106, 76, 74, 120, 79, 67, 70, 68, 53, 69, 66, 100, 59, 72, 72, 67, 71, 83, 72, 69, 72, 70, 60, 54, 65, 97, 67, 73, 68, 100, 101, 74, 94, 65, 61, 99, 68, 66, 66, 71, 53, 61, 59, 34, 34, 40, 101, 102, 71, 67, 72, 65, 72, 125, 62, 76, 60, 101, 66, 72, 50, 110, 77, 99, 73, 136, 100, 53, 40, 78, 69, 78, 50, 82, 60, 82, 60, 135, 123, 82, 59, 105, 90, 125, 62, 72, 44, 34, 0, 0, 0, 0, 64, 0, 0, 78, 58, 66, 53, 61, 56, 64, 53, 64, 53, 72, 58, 112, 100, 53, 40, 77, 67, 77, 67, 73, 58, 83, 63, 84, 72, 92, 67, 111, 84, 87, 69, 72, 44, 68, 54, 74, 65, 74, 65, 76, 67, 99, 82, 73, 56, 79, 68, 79, 71, 101, 79, 101, 79, 39, 183, 171, 74, 59, 75, 56, 78, 58, 78, 58, 73, 56, 94, 68, 28, 149, 138, 149, 138, 100, 72, 139, 138, 81, 66, 152, 138, 183, 171, 142, 130, 53, 46, 78, 58, 156, 143, 156, 140, 85, 69, 156, 140, 145, 132, 73, 50, 145, 136, 145, 136, 150, 140, 64, 53, 177, 165, 64, 45, 72, 50, 72, 50}, + }, + }, +} + +const FallbackAsciiCode int = 88 // 'X' + +func getCharacterWidth100(cCode int, typeface FontTypeface, weight FontWeight) float64 { + if weightMap, ok := sizeMap[typeface]; ok { + if rangeMap, ok := weightMap[weight]; ok { + if cCode < len(rangeMap[0]) { + return float64(rangeMap[0][cCode]) + } else if cCode >= 0x370 && cCode < 0x370+len(rangeMap[0x370]) { + return float64(rangeMap[0x370][cCode]) + } + return float64(rangeMap[0][FallbackAsciiCode]) + } + } + + return float64(sizeMap[FontTypefaceCourier][FontWeightNormal][0][FallbackAsciiCode]) +} + +/* +func getTextRunWidth(s string, typeface FontTypeface, weight FontWeight, fontSizeInPixels float64) float64 { + w := 0.0 + for _, c := range s { + w += float64(getCharacterWidth100(int(c), typeface, weight)*fontSizeInPixels) / 100.0 + } + return w +} + +func getTextDimensions(s string, typeface FontTypeface, weight FontWeight, fontSizeInPixels float64) (float64, float64) { + runs := strings.Split(s, "\n") + w := 0.0 + h := 0.0 + for _, r := range runs { + h += float64(fontSizeInPixels) + runWidth := getTextRunWidth(r, typeface, weight, fontSizeInPixels) + if w < runWidth { + w = runWidth + } + } + return w, h +} +*/ + +func getTextRunWidth(s string, startIdx int, endIdx int, typeface FontTypeface, weight FontWeight, fontSizeInPixels float64) float64 { + w := 0.0 + idx := startIdx + for idx < endIdx { + w += float64(getCharacterWidth100(int(s[idx]), typeface, weight)*fontSizeInPixels) / 100.0 + idx++ + } + return w +} + +func getTextDimensions(s string, typeface FontTypeface, weight FontWeight, fontSizeInPixels float64, fontInterval float64) (float64, float64) { + w := 0.0 + h := 0.0 + runStartIdx := -1 + lineCount := 0 + for curIdx, c := range s { + if c == '\n' { + if runStartIdx > -1 && curIdx-runStartIdx > 1 { + // Calculate run width for: from runStartIdx to curIdx-1 including + runWidth := getTextRunWidth(s, runStartIdx, curIdx, typeface, weight, fontSizeInPixels) + if runWidth > w { + w = runWidth + } + } + lineCount++ + h += float64(fontSizeInPixels) + runStartIdx = -1 + } else { + if runStartIdx == -1 { + runStartIdx = curIdx + } + } + } + + if runStartIdx > -1 && len(s)-runStartIdx >= 1 { + runWidth := getTextRunWidth(s, runStartIdx, len(s), typeface, weight, fontSizeInPixels) + if runWidth > w { + w = runWidth + } + } + if w > 0.0 { + lineCount++ + h += float64(fontSizeInPixels) + } else { + h = 0.0 + } + + if h > 0 { + h += float64(lineCount-1) * fontSizeInPixels * fontInterval + } + return w, h +} + +func getLabelDimensionsFromTextDimensions(w float64, h float64, textMargin float64) (float64, float64) { + labelWidth := 0.0 + if w > 0.0 { + labelWidth = w + textMargin*2 // left + right + } + labelHeight := h + if labelHeight > 0.0 { + labelHeight = h + textMargin*2 // top + bottom + } + return labelWidth, labelHeight +} diff --git a/pkg/capigraph/fonts_test.go b/pkg/capigraph/fonts_test.go new file mode 100644 index 00000000..9b6b46d7 --- /dev/null +++ b/pkg/capigraph/fonts_test.go @@ -0,0 +1,33 @@ +package capigraph + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTextDimension(t *testing.T) { + var w, h float64 + w, h = getTextDimensions("", FontTypefaceArial, FontWeightNormal, 20, 0.0) + assert.Equal(t, 0.0, w) + assert.Equal(t, 0.0, h) + w, h = getTextDimensions("A", FontTypefaceArial, FontWeightNormal, 20, 0.0) + assert.Equal(t, 13.4, w) + assert.Equal(t, 20.0, h) + w, h = getTextDimensions("\nA", FontTypefaceArial, FontWeightNormal, 20, 0.0) + assert.Equal(t, 13.4, w) + assert.Equal(t, 40.0, h) + w, h = getTextDimensions("\n", FontTypefaceArial, FontWeightNormal, 20, 0.0) + assert.Equal(t, 0.0, w) + assert.Equal(t, 0.0, h) +} + +func TestRunWidth(t *testing.T) { + var w float64 + w = getTextRunWidth("-A-------", 1, 2, FontTypefaceArial, FontWeightNormal, 20) + assert.Equal(t, 13.4, w) + w = getTextRunWidth("-AB------", 1, 3, FontTypefaceArial, FontWeightNormal, 20) + assert.Equal(t, 26.8, w) + w = getTextRunWidth("-\r-------", 1, 2, FontTypefaceArial, FontWeightNormal, 20) + assert.Equal(t, 5.6, w) +} diff --git a/pkg/capigraph/layer_mx.go b/pkg/capigraph/layer_mx.go new file mode 100644 index 00000000..9695e7fe --- /dev/null +++ b/pkg/capigraph/layer_mx.go @@ -0,0 +1,151 @@ +package capigraph + +import ( + "fmt" + "strings" +) + +const FakeNodeBase int16 = 10000 + +func sanitizeFakeNodeId(nodeId int16) int16 { + sanitizedNodeId := nodeId + if nodeId > FakeNodeBase { + sanitizedNodeId -= FakeNodeBase + } + return sanitizedNodeId +} + +type LayerMx [][]int16 + +func NewLayerMx(nodeDefs []NodeDef, nodeLayerMap []int, rootNodes []int16) (LayerMx, error) { + maxLayer := 0 + for i := range nodeDefs[1:] { + nodeId := int16(i + 1) + nodeLayer := nodeLayerMap[nodeId] + if nodeLayer > maxLayer { + maxLayer = nodeLayer + } + } + + layerMx := make(LayerMx, maxLayer+1) + + for layerIdx := range maxLayer + 1 { + layerMx[layerIdx] = make([]int16, 0, MaxLayerLen) + } + + // This will populate mx with a sample valid ids. As we overwrite top layers, bottom layers will become invalid. + // This is why we have a similar piece of code in mxIterRecursive. Let's keep this code though: + // it inserts fake nodes and calculates each layer size nicely + priChildrenMap := buildPriChildrenMap(nodeDefs) + for _, rootNodeId := range rootNodes { + layerMx.addNodeRecursively(rootNodeId, nodeLayerMap, priChildrenMap) + } + + for layerIdx := range len(layerMx) { + if len(layerMx[layerIdx]) > MaxLayerLen { + return nil, fmt.Errorf("cannot create NewLayerMx, too many nodes in row %d: %d; max allowed is %d", layerIdx, len(layerMx[layerIdx]), MaxLayerLen) + } + } + + return layerMx, nil +} + +func (mx LayerMx) String() string { + sb := strings.Builder{} + for i, row := range mx { + if i != 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("%d:%v", i, row)) + } + return sb.String() +} + +func (mx LayerMx) clone() LayerMx { + newMx := make(LayerMx, len(mx)) + for i, row := range mx { + newMx[i] = make([]int16, len(row)) + copy(newMx[i], row) + } + return newMx +} + +func halfByteToChar(b int8) rune { + if b < 10 { + return rune(int('0') + int(b)) + } + return rune(int('A') + int(b) - 10) +} + +func (mx LayerMx) signature() string { + sb := strings.Builder{} + for _, row := range mx { + rowRunes := make([]rune, len(row)*4) + for i, id := range row { + rowRunes[i*4] = halfByteToChar(int8(id >> 12)) + rowRunes[i*4+1] = halfByteToChar(int8((id >> 8) & 0x0F)) + rowRunes[i*4+2] = halfByteToChar(int8((id >> 4) & 0x0F)) + rowRunes[i*4+3] = halfByteToChar(int8(id & 0x0F)) + } + sb.WriteString(string(rowRunes)) + } + return sb.String() +} +func (mx LayerMx) addNodeRecursively(curNodeId int16, nodeLayerMap []int, priChildrenMap [][]int16) { + curLayerIdx := nodeLayerMap[curNodeId] + mx[curLayerIdx] = append(mx[curLayerIdx], curNodeId) + + for _, childId := range priChildrenMap[curNodeId] { + childLayerIdx := nodeLayerMap[childId] + if childLayerIdx > curLayerIdx+1 { + // Insert fake nodes when pri edge len > 1 + interLayerIdx := curLayerIdx + 1 + for interLayerIdx < childLayerIdx { + mx[interLayerIdx] = append(mx[interLayerIdx], childId+FakeNodeBase) + interLayerIdx++ + } + } + mx.addNodeRecursively(childId, nodeLayerMap, priChildrenMap) + } +} + +// TODO: this function is unused, although works nicely. Remove later. +func (mx LayerMx) isMonotonous(priParentMap []int16, totalNodes int) bool { + edgeStarts := make([]int16, MaxLayerLen) + edgeStartIndexes := make([]int, MaxLayerLen) + prevLayerNodeIdxMap := make([]int, totalNodes) + + for layerIdx := range len(mx) { + if layerIdx > 0 { + edgeStarts := edgeStarts[:len(mx[layerIdx])] + for j, edgeEndNodeId := range mx[layerIdx] { + edgeStarts[j] = priParentMap[sanitizeFakeNodeId(edgeEndNodeId)] + } + + edgeStartIndexes := edgeStartIndexes[:len(mx[layerIdx])] + for j, edgeStartIdx := range edgeStarts { + if edgeStartIdx == -1 { + // Simulate monotonous + if j == 0 { + edgeStartIndexes[j] = 0 + } else { + edgeStartIndexes[j] = edgeStartIndexes[j-1] + } + } else { + edgeStartIndexes[j] = prevLayerNodeIdxMap[sanitizeFakeNodeId(edgeStartIdx)] + } + } + + // Verify it's growing monotonously + for j := range len(edgeStartIndexes) - 1 { + if edgeStartIndexes[j] > edgeStartIndexes[j+1] { + return false + } + } + } + for idx, nodeId := range mx[layerIdx] { + prevLayerNodeIdxMap[sanitizeFakeNodeId(nodeId)] = idx + } + } + return true +} diff --git a/pkg/capigraph/layer_mx_permutator.go b/pkg/capigraph/layer_mx_permutator.go new file mode 100644 index 00000000..c4aae896 --- /dev/null +++ b/pkg/capigraph/layer_mx_permutator.go @@ -0,0 +1,195 @@ +package capigraph + +import "fmt" + +const MaxAllowedLayerPermutations int64 = 100000000 + +type LayerMxPermIterator struct { + Lps []*LayerPermutator // We need separate permutator for each layer to preserve state when we do recursion between layers + IntervalStarts [][]int + IntervalLengths [][]int + SrcMx LayerMx + WorkMx LayerMx + PriParentMap []int16 + PriChildrenMap [][]int16 + NodeLayerMap []int + RootNodes []int16 +} + +func NewLayerMxPermIterator(nodeDefs []NodeDef, srcMx LayerMx) (*LayerMxPermIterator, error) { + mxi := LayerMxPermIterator{} + mxi.Lps = make([]*LayerPermutator, len(srcMx)) + mxi.IntervalStarts = make([][]int, len(srcMx)) + mxi.IntervalLengths = make([][]int, len(srcMx)) + for i := range len(mxi.Lps) { + mxi.Lps[i] = NewLayerPermutator() + mxi.IntervalStarts[i] = make([]int, MaxIntervalsInLayer) + mxi.IntervalLengths[i] = make([]int, MaxIntervalsInLayer) + } + + mxi.SrcMx = srcMx + mxi.WorkMx = srcMx.clone() + mxi.PriParentMap = buildPriParentMap(nodeDefs) + mxi.PriChildrenMap = buildPriChildrenMap(nodeDefs) + mxi.NodeLayerMap = buildLayerMap(nodeDefs) + mxi.RootNodes = buildRootNodeList(mxi.PriParentMap) + + permutations := mxi.MxIteratorCount() + if permutations > MaxAllowedLayerPermutations { + return nil, fmt.Errorf("cannot create LayerMxPermIterator, too many permutations, only %d : %d", MaxAllowedLayerPermutations, permutations) + } + return &mxi, nil +} + +func harvestRowPermutationSettings(mxi *LayerMxPermIterator, layerIdx int) (int, int, int) { + var lastParent int16 + var lastParentFirstIdx int + totalIntervals := 0 + var insertStart, insertLen int + for i, curNodeId := range mxi.WorkMx[layerIdx] { + var curParent int16 + if curNodeId > FakeNodeBase { + curParent = mxi.PriParentMap[curNodeId-FakeNodeBase] + } else { + curParent = mxi.PriParentMap[curNodeId] + } + if curParent == MissingNodeId { + // This is a root + // Wrap up an interval for lastParent + if i-lastParentFirstIdx > 1 { + // Safeguard. Unlikely, but possible + if totalIntervals < len(mxi.IntervalStarts[layerIdx]) { + mxi.IntervalStarts[layerIdx][totalIntervals] = lastParentFirstIdx + mxi.IntervalLengths[layerIdx][totalIntervals] = i - lastParentFirstIdx + totalIntervals++ + } + } + lastParent = curParent + lastParentFirstIdx = i + + if insertLen == 0 { + insertStart = i + insertLen = 1 + } else { + insertLen++ + } + } else { + // This is not a root + + // Wrap up the interval, else - continuing current interval + if curParent != lastParent { + // Wrap up an interval for lastParent + if i-lastParentFirstIdx > 2 { + // Safeguard. Unlikely, but possible + if totalIntervals < len(mxi.IntervalStarts[layerIdx]) { + mxi.IntervalStarts[layerIdx][totalIntervals] = lastParentFirstIdx + mxi.IntervalLengths[layerIdx][totalIntervals] = i - lastParentFirstIdx + totalIntervals++ + } + } + lastParent = curParent + lastParentFirstIdx = i + } + } + + if i == len(mxi.WorkMx[layerIdx])-1 { + // If the last id is not root and it's the ending of an interval - wrap it up here + if curParent != MissingNodeId && i > lastParentFirstIdx { + // Safeguard. Unlikely, but possible + if totalIntervals < len(mxi.IntervalStarts[layerIdx]) { + mxi.IntervalStarts[layerIdx][totalIntervals] = lastParentFirstIdx + mxi.IntervalLengths[layerIdx][totalIntervals] = i - lastParentFirstIdx + 1 + totalIntervals++ + } + } + } + } + return totalIntervals, insertStart, insertLen +} + +func (mxi *LayerMxPermIterator) MxIterator(f func(int, LayerMx)) { + mxIterRecursive(mxi, 0, 0, f) +} + +func mxIterRecursive(mxi *LayerMxPermIterator, layerIdx int, totalCnt int, f func(totalCnt int, perm LayerMx)) { + cbInner := func(int, LayerMx) { + f(totalCnt, mxi.WorkMx) + totalCnt++ + } + cb := func(int, []int16) { + mxi.WorkMx[layerIdx] = mxi.Lps[layerIdx].WorkPerm + mxIterRecursive(mxi, layerIdx+1, totalCnt, cbInner) + } + if layerIdx == len(mxi.SrcMx) { + f(totalCnt, mxi.WorkMx) + totalCnt++ + return + } + + newNodeIdx := 0 + if layerIdx > 0 { + for _, nodeId := range mxi.WorkMx[layerIdx-1] { + if nodeId > FakeNodeBase { + // Fake parent node, has only one child. Decide either this (layerIdx) child is fake or not. + childLayer := mxi.NodeLayerMap[nodeId-FakeNodeBase] + if childLayer == layerIdx { + mxi.WorkMx[layerIdx][newNodeIdx] = nodeId - FakeNodeBase // This is the true node + } else { + mxi.WorkMx[layerIdx][newNodeIdx] = nodeId // Keep faking, keep in mind that nodeId will have children that reference same Def.NodeId + } + newNodeIdx++ + } else { + // Normal (non-fake) parent + children := mxi.PriChildrenMap[nodeId] + for _, childId := range children { + childLayer := mxi.NodeLayerMap[childId] + if childLayer > layerIdx { + // Add another fake node until childLayer == layerIdx + mxi.WorkMx[layerIdx][newNodeIdx] = childId + FakeNodeBase + } else { + // Add the real node, childLayer == layerIdx + mxi.WorkMx[layerIdx][newNodeIdx] = childId + } + newNodeIdx++ + } + } + } + } + + // Add new roots + for _, rootId := range mxi.RootNodes { + if mxi.NodeLayerMap[rootId] == layerIdx { + mxi.WorkMx[layerIdx][newNodeIdx] = rootId + newNodeIdx++ + } + } + + totalIntervals, insertStart, insertLen := harvestRowPermutationSettings(mxi, layerIdx) + if totalIntervals > 0 && insertLen > 0 && insertStart >= 0 { + mxi.Lps[layerIdx].SwapAndInsertIterator(mxi.IntervalStarts[layerIdx], mxi.IntervalLengths[layerIdx], totalIntervals, insertStart, insertLen, mxi.WorkMx[layerIdx], cb) + } else if totalIntervals > 0 { + mxi.Lps[layerIdx].SwapIterator(mxi.IntervalStarts[layerIdx], mxi.IntervalLengths[layerIdx], totalIntervals, mxi.WorkMx[layerIdx], cb) + } else if insertLen > 0 { + mxi.Lps[layerIdx].InsertIterator(insertStart, insertLen, mxi.WorkMx[layerIdx], cb) + } else { + // No permutations available, just re-use mxi.WorkMx[layerIdx] without modifications + // Here, we want to call cb, but without mxi.WorkMx[layerIdx] = mxi.Lps[layerIdx].WorkPerm + mxIterRecursive(mxi, layerIdx+1, totalCnt, cbInner) + } +} + +func (mxi *LayerMxPermIterator) MxIteratorCount() int64 { + acc := int64(1) + for layerIdx := range len(mxi.SrcMx) { + totalIntervals, insertStart, insertLen := harvestRowPermutationSettings(mxi, layerIdx) + for i := range totalIntervals { + intervalLen := mxi.IntervalLengths[layerIdx][i] + acc *= int64(mxi.Lps[layerIdx].Fact[intervalLen]) + } + for range insertLen { + acc *= int64(insertStart + 1) + insertStart++ + } + } + return acc +} diff --git a/pkg/capigraph/layer_mx_permutator_test.go b/pkg/capigraph/layer_mx_permutator_test.go new file mode 100644 index 00000000..20ec4935 --- /dev/null +++ b/pkg/capigraph/layer_mx_permutator_test.go @@ -0,0 +1,313 @@ +package capigraph + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Common MxPermutator and SVG tests + +func helperAll(t *testing.T, + nodeDefs []NodeDef, + expectedLayerMap string, + expectedStartMx string, + expectedIterCount int64, + expectedPermMxs string, + expectedHierarchyFirst string, + expectedHierarchyLast string) { + priParentMap := buildPriParentMap(nodeDefs) + layerMap := buildLayerMap(nodeDefs) + assert.Equal(t, expectedLayerMap, fmt.Sprintf("%v", layerMap)) + + rootNodes := buildRootNodeList(priParentMap) + mx, _ := NewLayerMx(nodeDefs, layerMap, rootNodes) + assert.Equal(t, expectedStartMx, mx.String()) + + mxi, _ := NewLayerMxPermIterator(nodeDefs, mx) + vnh := NewVizNodeHierarchy(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions()) + + vnh.buildNewRootSubtreeHierarchy(mx) + + sbPerms := strings.Builder{} + var hierarchyFirst, hierarchyLast string + mxi.MxIterator(func(i int, mxPerm LayerMx) { + if sbPerms.Len() != 0 { + sbPerms.WriteString(", ") + } + sbPerms.WriteString(fmt.Sprintf("{p%d: {%s}}", i, mxPerm.String())) + vnh.reuseRootSubtreeHierarchy(mxPerm) + vnh.PopulateNodeTotalWidth() + vnh.PopulateNodesXCoords() + vnh.PopulateEdgeLabelDimensions() + vnh.PopulateUpperLayerGapMap(DefaultEdgeLabelFontOptions().SizeInPixels) + vnh.PopulateNodesYCoords() + hierarchyString := vnh.String() + if hierarchyFirst == "" { + hierarchyFirst = hierarchyString + } + hierarchyLast = hierarchyString + }) + + assert.Equal(t, expectedIterCount, mxi.MxIteratorCount()) + assert.Equal(t, expectedPermMxs, sbPerms.String()) + assert.Equal(t, expectedHierarchyFirst, hierarchyFirst) + assert.Equal(t, expectedHierarchyLast, hierarchyLast) +} + +func helperIteratorAndIncrementalCount(t *testing.T, + nodeDefs []NodeDef, + expectedLayerMap string, + expectedStartMx string, + expectedIterCount int64, + expectedIncrementalCount int64) { + priParentMap := buildPriParentMap(nodeDefs) + layerMap := buildLayerMap(nodeDefs) + assert.Equal(t, expectedLayerMap, fmt.Sprintf("%v", layerMap)) + + rootNodes := buildRootNodeList(priParentMap) + mx, _ := NewLayerMx(nodeDefs, layerMap, rootNodes) + assert.Equal(t, expectedStartMx, mx.String()) + + mxi, _ := NewLayerMxPermIterator(nodeDefs, mx) + + cnt := int64(0) + mxi.MxIterator(func(_ int, _ LayerMx) { + cnt++ + }) + + assert.Equal(t, expectedIterCount, mxi.MxIteratorCount()) + assert.Equal(t, expectedIncrementalCount, cnt) +} + +func TestBasicMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsBasic, + "[-4 0 1 0]", + "0:[1 3], 1:[2]", + int64(2), + "{p0: {0:[1 3], 1:[2]}}, {p1: {0:[3 1], 1:[2]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:3 X:0.00 Y:0.00 W:69.12 H:36.00}] PriRootOut:[]}, {Id:3 RootId:3 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:3 X:0.00 Y:0.00 W:69.12 H:36.00}] PriRootOut:[]}, {Id:3 RootId:3 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}", + ) +} + +func TestTrivialParallelMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsTrivialParallel, + "[-4 0 1 0 1]", + "0:[1 3], 1:[2 4]", + int64(2), + "{p0: {0:[1 3], 1:[2 4]}}, {p1: {0:[3 1], 1:[4 2]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:3 RootId:3 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[4]}, {Id:4 RootId:3 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:3 RootId:3 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[4]}, {Id:4 RootId:3 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestOneEnclosingOneLevelMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsOneEnclosedOneLevel, + "[-4 0 1 1 2 2 1]", + "0:[1], 1:[2 3 6], 2:[4 5]", + int64(6), + "{p0: {0:[1], 1:[2 3 6], 2:[4 5]}}, {p1: {0:[1], 1:[2 6 3], 2:[4 5]}}, {p2: {0:[1], 1:[6 2 3], 2:[4 5]}}, {p3: {0:[1], 1:[3 2 6], 2:[5 4]}}, {p4: {0:[1], 1:[3 6 2], 2:[5 4]}}, {p5: {0:[1], 1:[6 3 2], 2:[5 4]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 3]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:3 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:4 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:144.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[3 2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:3 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:4 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:144.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}") +} + +func TestOneEnclosedTwoLevelsMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsOneEnclosedTwoLevels, + "[-4 0 1 2 3 1 2 3 2]", + "0:[1], 1:[2 5], 2:[3 6 8], 3:[4 7]", + int64(6), + "{p0: {0:[1], 1:[2 5], 2:[3 6 8], 3:[4 7]}}, {p1: {0:[1], 1:[2 5], 2:[3 8 6], 3:[4 7]}}, {p2: {0:[1], 1:[2 5], 2:[8 3 6], 3:[4 7]}}, {p3: {0:[1], 1:[5 2], 2:[6 3 8], 3:[7 4]}}, {p4: {0:[1], 1:[5 2], 2:[6 8 3], 3:[7 4]}}, {p5: {0:[1], 1:[5 2], 2:[8 6 3], 3:[7 4]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 5]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[6]}, {Id:6 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:8 RootId:8 Layer:2 X:144.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[5 2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[6]}, {Id:6 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:8 RootId:8 Layer:2 X:144.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}") +} + +func TestNoIntervalsMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsNoIntervals, + "[-4 0 1 2 1 2]", + "0:[1], 1:[2 4], 2:[3 5]", + int64(2), + "{p0: {0:[1], 1:[2 4], 2:[3 5]}}, {p1: {0:[1], 1:[4 2], 2:[5 3]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 4]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:4 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[4 2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:4 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestFlat10MxPermutator(t *testing.T) { + helperIteratorAndIncrementalCount(t, + testNodeDefsFlat10, + "[-4 0 0 0 0 0 0 0 0 0 0]", + "0:[1 2 3 4 5 6 7 8 9 10]", + int64(3628800), + int64(3628800)) +} + +func TestTwoEnclosingTwoLevelsNodeSizeMattersMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsTwoEnclosedNodeSizeMatters, + "[-4 0 1 1 2 2 3 3 2 2]", + "0:[1], 1:[2 3], 2:[4 5 8 9], 3:[6 7]", + int64(24), + "{p0: {0:[1], 1:[2 3], 2:[4 5 8 9], 3:[6 7]}}, {p1: {0:[1], 1:[2 3], 2:[4 5 9 8], 3:[6 7]}}, {p2: {0:[1], 1:[2 3], 2:[4 9 5 8], 3:[6 7]}}, {p3: {0:[1], 1:[2 3], 2:[9 4 5 8], 3:[6 7]}}, {p4: {0:[1], 1:[2 3], 2:[4 8 5 9], 3:[6 7]}}, {p5: {0:[1], 1:[2 3], 2:[4 8 9 5], 3:[6 7]}}, {p6: {0:[1], 1:[2 3], 2:[4 9 8 5], 3:[6 7]}}, {p7: {0:[1], 1:[2 3], 2:[9 4 8 5], 3:[6 7]}}, {p8: {0:[1], 1:[2 3], 2:[8 4 5 9], 3:[6 7]}}, {p9: {0:[1], 1:[2 3], 2:[8 4 9 5], 3:[6 7]}}, {p10: {0:[1], 1:[2 3], 2:[8 9 4 5], 3:[6 7]}}, {p11: {0:[1], 1:[2 3], 2:[9 8 4 5], 3:[6 7]}}, {p12: {0:[1], 1:[3 2], 2:[5 4 8 9], 3:[7 6]}}, {p13: {0:[1], 1:[3 2], 2:[5 4 9 8], 3:[7 6]}}, {p14: {0:[1], 1:[3 2], 2:[5 9 4 8], 3:[7 6]}}, {p15: {0:[1], 1:[3 2], 2:[9 5 4 8], 3:[7 6]}}, {p16: {0:[1], 1:[3 2], 2:[5 8 4 9], 3:[7 6]}}, {p17: {0:[1], 1:[3 2], 2:[5 8 9 4], 3:[7 6]}}, {p18: {0:[1], 1:[3 2], 2:[5 9 8 4], 3:[7 6]}}, {p19: {0:[1], 1:[3 2], 2:[9 5 8 4], 3:[7 6]}}, {p20: {0:[1], 1:[3 2], 2:[8 5 4 9], 3:[7 6]}}, {p21: {0:[1], 1:[3 2], 2:[8 5 9 4], 3:[7 6]}}, {p22: {0:[1], 1:[3 2], 2:[8 9 5 4], 3:[7 6]}}, {p23: {0:[1], 1:[3 2], 2:[9 8 5 4], 3:[7 6]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 3]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:3 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:4 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[6]}, {Id:5 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:6 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:9 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:7 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:9 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:8 RootId:8 Layer:2 X:144.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}, {Id:9 RootId:9 Layer:2 X:216.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[3 2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:3 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:4 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[6]}, {Id:5 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:6 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:9 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:7 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:9 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:8 RootId:8 Layer:2 X:216.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}, {Id:9 RootId:9 Layer:2 X:144.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}") +} + +func TestOneSecondaryMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsOneSecondary, + "[-4 0 1 0 1 2 1]", + "0:[1 3], 1:[2 4 6], 2:[5]", + int64(6), + "{p0: {0:[1 3], 1:[2 4 6], 2:[5]}}, {p1: {0:[1 3], 1:[2 6 4], 2:[5]}}, {p2: {0:[1 3], 1:[6 2 4], 2:[5]}}, {p3: {0:[3 1], 1:[4 2 6], 2:[5]}}, {p4: {0:[3 1], 1:[4 6 2], 2:[5]}}, {p5: {0:[3 1], 1:[6 4 2], 2:[5]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:3 RootId:3 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[4]}, {Id:4 RootId:3 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:3 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:144.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:3 RootId:3 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[4]}, {Id:4 RootId:3 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:3 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:144.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}") +} + +func TestDiamonMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsDiamond, + "[-4 0 1 1 1 2 1]", + "0:[1], 1:[2 3 4 6], 2:[5]", + int64(24), + "{p0: {0:[1], 1:[2 3 4 6], 2:[5]}}, {p1: {0:[1], 1:[2 3 6 4], 2:[5]}}, {p2: {0:[1], 1:[2 6 3 4], 2:[5]}}, {p3: {0:[1], 1:[6 2 3 4], 2:[5]}}, {p4: {0:[1], 1:[2 4 3 6], 2:[5]}}, {p5: {0:[1], 1:[2 4 6 3], 2:[5]}}, {p6: {0:[1], 1:[2 6 4 3], 2:[5]}}, {p7: {0:[1], 1:[6 2 4 3], 2:[5]}}, {p8: {0:[1], 1:[4 3 2 6], 2:[5]}}, {p9: {0:[1], 1:[4 3 6 2], 2:[5]}}, {p10: {0:[1], 1:[4 6 3 2], 2:[5]}}, {p11: {0:[1], 1:[6 4 3 2], 2:[5]}}, {p12: {0:[1], 1:[3 2 4 6], 2:[5]}}, {p13: {0:[1], 1:[3 2 6 4], 2:[5]}}, {p14: {0:[1], 1:[3 6 2 4], 2:[5]}}, {p15: {0:[1], 1:[6 3 2 4], 2:[5]}}, {p16: {0:[1], 1:[3 4 2 6], 2:[5]}}, {p17: {0:[1], 1:[3 4 6 2], 2:[5]}}, {p18: {0:[1], 1:[3 6 4 2], 2:[5]}}, {p19: {0:[1], 1:[6 3 4 2], 2:[5]}}, {p20: {0:[1], 1:[4 2 3 6], 2:[5]}}, {p21: {0:[1], 1:[4 2 6 3], 2:[5]}}, {p22: {0:[1], 1:[4 6 2 3], 2:[5]}}, {p23: {0:[1], 1:[6 4 2 3], 2:[5]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:196.00 W:52.00 H:60.00 In:[] PriRootOut:[2 3 4]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:3 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:4 RootId:1 Layer:1 X:144.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:216.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:196.00 W:52.00 H:60.00 In:[] PriRootOut:[4 2 3]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:3 RootId:1 Layer:1 X:144.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:4 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:2 X:144.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:216.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}") +} + +func TestSubtreeBelowLongMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsSubtreeBelowLong, + "[-4 0 1 2 3 2 3 4]", + "0:[1], 1:[2], 2:[3 5], 3:[4 6], 4:[7]", + int64(2), + "{p0: {0:[1], 1:[2], 2:[3 5], 3:[4 6], 4:[7]}}, {p1: {0:[1], 1:[2], 2:[5 3], 3:[6 4], 4:[7]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:5 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[6]}, {Id:6 RootId:5 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:5 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:5 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[6]}, {Id:6 RootId:5 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:5 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestOneNotTwoLevelsDownMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsOneNotTwoLevelsDown, + "[-4 0 1 2 3 4 1 2 3]", + "0:[1], 1:[2 6], 2:[3 7], 3:[4 8], 4:[5]", + int64(2), + "{p0: {0:[1], 1:[2 6], 2:[3 7], 3:[4 8], 4:[5]}}, {p1: {0:[1], 1:[6 2], 2:[7 3], 3:[8 4], 4:[5]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:1 Layer:4 X:0.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[7]}, {Id:7 RootId:6 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[8]}, {Id:8 RootId:6 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:7 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:1 Layer:4 X:0.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:6 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[7]}, {Id:7 RootId:6 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[8]}, {Id:8 RootId:6 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:7 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestMultiSecParentPullDownMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsMultiSecParentPullDown, + "[-4 0 1 2 3 1 2]", + "0:[1], 1:[2 5], 2:[3 6], 3:[4]", + int64(2), + "{p0: {0:[1], 1:[2 5], 2:[3 6], 3:[4]}}, {p1: {0:[1], 1:[5 2], 2:[6 3], 3:[4]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:5 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[6]}, {Id:6 RootId:5 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:5 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[6]}, {Id:6 RootId:5 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestMultiSecParentNoPullDownMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsMultiSecParentNoPullDown, + "[-4 0 1 2 0 1]", + "0:[1 4], 1:[2 5], 2:[3]", + int64(2), + "{p0: {0:[1 4], 1:[2 5], 2:[3]}}, {p1: {0:[4 1], 1:[5 2], 2:[3]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:4 RootId:4 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[5]}, {Id:5 RootId:4 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:4 RootId:4 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[5]}, {Id:5 RootId:4 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestTwoLevelsFromOneParentMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsTwoLevelsFromOneParent, + "[-4 0 0 1 2]", + "0:[1 2], 1:[3], 2:[4]", + int64(2), + "{p0: {0:[1 2], 1:[3], 2:[4]}}, {p1: {0:[2 1], 1:[3], 2:[4]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[3]}, {Id:2 RootId:2 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}, {Id:3 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[3]}, {Id:2 RootId:2 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}, {Id:3 RootId:1 Layer:1 X:72.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestTwoLevelsFromOneParentSameRootMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsTwoLevelsFromOneParentSameRoot, + "[-4 0 1 2 3 2 3 4]", + "0:[1], 1:[2 10005], 2:[3 5], 3:[4 6], 4:[7]", + int64(2), + "{p0: {0:[1], 1:[2 10005], 2:[3 5], 3:[4 6], 4:[7]}}, {p1: {0:[1], 1:[10005 2], 2:[5 3], 3:[6 4], 4:[7]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 5]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[6]}, {Id:6 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:1 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 5]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[6]}, {Id:6 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:1 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestTwoLevelsFromOneParentSameRootTwoFakesMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsTwoLevelsFromOneParentSameRootTwoFakes, + "[-4 0 1 2 3 4 3 4 5]", + "0:[1], 1:[2 10006], 2:[3 10006], 3:[4 6], 4:[5 7], 5:[8]", + int64(2), + "{p0: {0:[1], 1:[2 10006], 2:[3 10006], 3:[4 6], 4:[5 7], 5:[8]}}, {p1: {0:[1], 1:[10006 2], 2:[10006 3], 3:[6 4], 4:[7 5], 5:[8]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 6]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:1 Layer:4 X:0.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:1 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[8]}, {Id:8 RootId:1 Layer:5 X:72.00 Y:600.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:7 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:124.00 W:52.00 H:60.00 In:[] PriRootOut:[2 6]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5]}, {Id:5 RootId:1 Layer:4 X:0.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:1 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[8]}, {Id:8 RootId:1 Layer:5 X:72.00 Y:600.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:7 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestDuplicateSecLabelsMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsDuplicateSecLabels, + "[-4 0 0 0 0 0 0 1 1 1 1 1]", + "0:[1 2 3 4 5 6], 1:[7 8 9 10 11]", + int64(720), + "{p0: {0:[1 2 3 4 5 6], 1:[7 8 9 10 11]}}, {p1: {0:[1 2 3 4 6 5], 1:[7 8 9 11 10]}}, {p2: {0:[1 2 3 6 4 5], 1:[7 8 11 9 10]}}, {p3: {0:[1 2 6 3 4 5], 1:[7 11 8 9 10]}}, {p4: {0:[1 6 2 3 4 5], 1:[11 7 8 9 10]}}, {p5: {0:[6 1 2 3 4 5], 1:[11 7 8 9 10]}}, {p6: {0:[1 2 3 5 4 6], 1:[7 8 10 9 11]}}, {p7: {0:[1 2 3 5 6 4], 1:[7 8 10 11 9]}}, {p8: {0:[1 2 3 6 5 4], 1:[7 8 11 10 9]}}, {p9: {0:[1 2 6 3 5 4], 1:[7 11 8 10 9]}}, {p10: {0:[1 6 2 3 5 4], 1:[11 7 8 10 9]}}, {p11: {0:[6 1 2 3 5 4], 1:[11 7 8 10 9]}}, {p12: {0:[1 2 5 3 4 6], 1:[7 10 8 9 11]}}, {p13: {0:[1 2 5 3 6 4], 1:[7 10 8 11 9]}}, {p14: {0:[1 2 5 6 3 4], 1:[7 10 11 8 9]}}, {p15: {0:[1 2 6 5 3 4], 1:[7 11 10 8 9]}}, {p16: {0:[1 6 2 5 3 4], 1:[11 7 10 8 9]}}, {p17: {0:[6 1 2 5 3 4], 1:[11 7 10 8 9]}}, {p18: {0:[1 5 2 3 4 6], 1:[10 7 8 9 11]}}, {p19: {0:[1 5 2 3 6 4], 1:[10 7 8 11 9]}}, {p20: {0:[1 5 2 6 3 4], 1:[10 7 11 8 9]}}, {p21: {0:[1 5 6 2 3 4], 1:[10 11 7 8 9]}}, {p22: {0:[1 6 5 2 3 4], 1:[11 10 7 8 9]}}, {p23: {0:[6 1 5 2 3 4], 1:[11 10 7 8 9]}}, {p24: {0:[5 1 2 3 4 6], 1:[10 7 8 9 11]}}, {p25: {0:[5 1 2 3 6 4], 1:[10 7 8 11 9]}}, {p26: {0:[5 1 2 6 3 4], 1:[10 7 11 8 9]}}, {p27: {0:[5 1 6 2 3 4], 1:[10 11 7 8 9]}}, {p28: {0:[5 6 1 2 3 4], 1:[10 11 7 8 9]}}, {p29: {0:[6 5 1 2 3 4], 1:[11 10 7 8 9]}}, {p30: {0:[1 2 4 3 5 6], 1:[7 9 8 10 11]}}, {p31: {0:[1 2 4 3 6 5], 1:[7 9 8 11 10]}}, {p32: {0:[1 2 4 6 3 5], 1:[7 9 11 8 10]}}, {p33: {0:[1 2 6 4 3 5], 1:[7 11 9 8 10]}}, {p34: {0:[1 6 2 4 3 5], 1:[11 7 9 8 10]}}, {p35: {0:[6 1 2 4 3 5], 1:[11 7 9 8 10]}}, {p36: {0:[1 2 4 5 3 6], 1:[7 9 10 8 11]}}, {p37: {0:[1 2 4 5 6 3], 1:[7 9 10 11 8]}}, {p38: {0:[1 2 4 6 5 3], 1:[7 9 11 10 8]}}, {p39: {0:[1 2 6 4 5 3], 1:[7 11 9 10 8]}}, {p40: {0:[1 6 2 4 5 3], 1:[11 7 9 10 8]}}, {p41: {0:[6 1 2 4 5 3], 1:[11 7 9 10 8]}}, {p42: {0:[1 2 5 4 3 6], 1:[7 10 9 8 11]}}, {p43: {0:[1 2 5 4 6 3], 1:[7 10 9 11 8]}}, {p44: {0:[1 2 5 6 4 3], 1:[7 10 11 9 8]}}, {p45: {0:[1 2 6 5 4 3], 1:[7 11 10 9 8]}}, {p46: {0:[1 6 2 5 4 3], 1:[11 7 10 9 8]}}, {p47: {0:[6 1 2 5 4 3], 1:[11 7 10 9 8]}}, {p48: {0:[1 5 2 4 3 6], 1:[10 7 9 8 11]}}, {p49: {0:[1 5 2 4 6 3], 1:[10 7 9 11 8]}}, {p50: {0:[1 5 2 6 4 3], 1:[10 7 11 9 8]}}, {p51: {0:[1 5 6 2 4 3], 1:[10 11 7 9 8]}}, {p52: {0:[1 6 5 2 4 3], 1:[11 10 7 9 8]}}, {p53: {0:[6 1 5 2 4 3], 1:[11 10 7 9 8]}}, {p54: {0:[5 1 2 4 3 6], 1:[10 7 9 8 11]}}, {p55: {0:[5 1 2 4 6 3], 1:[10 7 9 11 8]}}, {p56: {0:[5 1 2 6 4 3], 1:[10 7 11 9 8]}}, {p57: {0:[5 1 6 2 4 3], 1:[10 11 7 9 8]}}, {p58: {0:[5 6 1 2 4 3], 1:[10 11 7 9 8]}}, {p59: {0:[6 5 1 2 4 3], 1:[11 10 7 9 8]}}, {p60: {0:[1 4 2 3 5 6], 1:[9 7 8 10 11]}}, {p61: {0:[1 4 2 3 6 5], 1:[9 7 8 11 10]}}, {p62: {0:[1 4 2 6 3 5], 1:[9 7 11 8 10]}}, {p63: {0:[1 4 6 2 3 5], 1:[9 11 7 8 10]}}, {p64: {0:[1 6 4 2 3 5], 1:[11 9 7 8 10]}}, {p65: {0:[6 1 4 2 3 5], 1:[11 9 7 8 10]}}, {p66: {0:[1 4 2 5 3 6], 1:[9 7 10 8 11]}}, {p67: {0:[1 4 2 5 6 3], 1:[9 7 10 11 8]}}, {p68: {0:[1 4 2 6 5 3], 1:[9 7 11 10 8]}}, {p69: {0:[1 4 6 2 5 3], 1:[9 11 7 10 8]}}, {p70: {0:[1 6 4 2 5 3], 1:[11 9 7 10 8]}}, {p71: {0:[6 1 4 2 5 3], 1:[11 9 7 10 8]}}, {p72: {0:[1 4 5 2 3 6], 1:[9 10 7 8 11]}}, {p73: {0:[1 4 5 2 6 3], 1:[9 10 7 11 8]}}, {p74: {0:[1 4 5 6 2 3], 1:[9 10 11 7 8]}}, {p75: {0:[1 4 6 5 2 3], 1:[9 11 10 7 8]}}, {p76: {0:[1 6 4 5 2 3], 1:[11 9 10 7 8]}}, {p77: {0:[6 1 4 5 2 3], 1:[11 9 10 7 8]}}, {p78: {0:[1 5 4 2 3 6], 1:[10 9 7 8 11]}}, {p79: {0:[1 5 4 2 6 3], 1:[10 9 7 11 8]}}, {p80: {0:[1 5 4 6 2 3], 1:[10 9 11 7 8]}}, {p81: {0:[1 5 6 4 2 3], 1:[10 11 9 7 8]}}, {p82: {0:[1 6 5 4 2 3], 1:[11 10 9 7 8]}}, {p83: {0:[6 1 5 4 2 3], 1:[11 10 9 7 8]}}, {p84: {0:[5 1 4 2 3 6], 1:[10 9 7 8 11]}}, {p85: {0:[5 1 4 2 6 3], 1:[10 9 7 11 8]}}, {p86: {0:[5 1 4 6 2 3], 1:[10 9 11 7 8]}}, {p87: {0:[5 1 6 4 2 3], 1:[10 11 9 7 8]}}, {p88: {0:[5 6 1 4 2 3], 1:[10 11 9 7 8]}}, {p89: {0:[6 5 1 4 2 3], 1:[11 10 9 7 8]}}, {p90: {0:[4 1 2 3 5 6], 1:[9 7 8 10 11]}}, {p91: {0:[4 1 2 3 6 5], 1:[9 7 8 11 10]}}, {p92: {0:[4 1 2 6 3 5], 1:[9 7 11 8 10]}}, {p93: {0:[4 1 6 2 3 5], 1:[9 11 7 8 10]}}, {p94: {0:[4 6 1 2 3 5], 1:[9 11 7 8 10]}}, {p95: {0:[6 4 1 2 3 5], 1:[11 9 7 8 10]}}, {p96: {0:[4 1 2 5 3 6], 1:[9 7 10 8 11]}}, {p97: {0:[4 1 2 5 6 3], 1:[9 7 10 11 8]}}, {p98: {0:[4 1 2 6 5 3], 1:[9 7 11 10 8]}}, {p99: {0:[4 1 6 2 5 3], 1:[9 11 7 10 8]}}, {p100: {0:[4 6 1 2 5 3], 1:[9 11 7 10 8]}}, {p101: {0:[6 4 1 2 5 3], 1:[11 9 7 10 8]}}, {p102: {0:[4 1 5 2 3 6], 1:[9 10 7 8 11]}}, {p103: {0:[4 1 5 2 6 3], 1:[9 10 7 11 8]}}, {p104: {0:[4 1 5 6 2 3], 1:[9 10 11 7 8]}}, {p105: {0:[4 1 6 5 2 3], 1:[9 11 10 7 8]}}, {p106: {0:[4 6 1 5 2 3], 1:[9 11 10 7 8]}}, {p107: {0:[6 4 1 5 2 3], 1:[11 9 10 7 8]}}, {p108: {0:[4 5 1 2 3 6], 1:[9 10 7 8 11]}}, {p109: {0:[4 5 1 2 6 3], 1:[9 10 7 11 8]}}, {p110: {0:[4 5 1 6 2 3], 1:[9 10 11 7 8]}}, {p111: {0:[4 5 6 1 2 3], 1:[9 10 11 7 8]}}, {p112: {0:[4 6 5 1 2 3], 1:[9 11 10 7 8]}}, {p113: {0:[6 4 5 1 2 3], 1:[11 9 10 7 8]}}, {p114: {0:[5 4 1 2 3 6], 1:[10 9 7 8 11]}}, {p115: {0:[5 4 1 2 6 3], 1:[10 9 7 11 8]}}, {p116: {0:[5 4 1 6 2 3], 1:[10 9 11 7 8]}}, {p117: {0:[5 4 6 1 2 3], 1:[10 9 11 7 8]}}, {p118: {0:[5 6 4 1 2 3], 1:[10 11 9 7 8]}}, {p119: {0:[6 5 4 1 2 3], 1:[11 10 9 7 8]}}, {p120: {0:[1 3 2 4 5 6], 1:[8 7 9 10 11]}}, {p121: {0:[1 3 2 4 6 5], 1:[8 7 9 11 10]}}, {p122: {0:[1 3 2 6 4 5], 1:[8 7 11 9 10]}}, {p123: {0:[1 3 6 2 4 5], 1:[8 11 7 9 10]}}, {p124: {0:[1 6 3 2 4 5], 1:[11 8 7 9 10]}}, {p125: {0:[6 1 3 2 4 5], 1:[11 8 7 9 10]}}, {p126: {0:[1 3 2 5 4 6], 1:[8 7 10 9 11]}}, {p127: {0:[1 3 2 5 6 4], 1:[8 7 10 11 9]}}, {p128: {0:[1 3 2 6 5 4], 1:[8 7 11 10 9]}}, {p129: {0:[1 3 6 2 5 4], 1:[8 11 7 10 9]}}, {p130: {0:[1 6 3 2 5 4], 1:[11 8 7 10 9]}}, {p131: {0:[6 1 3 2 5 4], 1:[11 8 7 10 9]}}, {p132: {0:[1 3 5 2 4 6], 1:[8 10 7 9 11]}}, {p133: {0:[1 3 5 2 6 4], 1:[8 10 7 11 9]}}, {p134: {0:[1 3 5 6 2 4], 1:[8 10 11 7 9]}}, {p135: {0:[1 3 6 5 2 4], 1:[8 11 10 7 9]}}, {p136: {0:[1 6 3 5 2 4], 1:[11 8 10 7 9]}}, {p137: {0:[6 1 3 5 2 4], 1:[11 8 10 7 9]}}, {p138: {0:[1 5 3 2 4 6], 1:[10 8 7 9 11]}}, {p139: {0:[1 5 3 2 6 4], 1:[10 8 7 11 9]}}, {p140: {0:[1 5 3 6 2 4], 1:[10 8 11 7 9]}}, {p141: {0:[1 5 6 3 2 4], 1:[10 11 8 7 9]}}, {p142: {0:[1 6 5 3 2 4], 1:[11 10 8 7 9]}}, {p143: {0:[6 1 5 3 2 4], 1:[11 10 8 7 9]}}, {p144: {0:[5 1 3 2 4 6], 1:[10 8 7 9 11]}}, {p145: {0:[5 1 3 2 6 4], 1:[10 8 7 11 9]}}, {p146: {0:[5 1 3 6 2 4], 1:[10 8 11 7 9]}}, {p147: {0:[5 1 6 3 2 4], 1:[10 11 8 7 9]}}, {p148: {0:[5 6 1 3 2 4], 1:[10 11 8 7 9]}}, {p149: {0:[6 5 1 3 2 4], 1:[11 10 8 7 9]}}, {p150: {0:[1 3 4 2 5 6], 1:[8 9 7 10 11]}}, {p151: {0:[1 3 4 2 6 5], 1:[8 9 7 11 10]}}, {p152: {0:[1 3 4 6 2 5], 1:[8 9 11 7 10]}}, {p153: {0:[1 3 6 4 2 5], 1:[8 11 9 7 10]}}, {p154: {0:[1 6 3 4 2 5], 1:[11 8 9 7 10]}}, {p155: {0:[6 1 3 4 2 5], 1:[11 8 9 7 10]}}, {p156: {0:[1 3 4 5 2 6], 1:[8 9 10 7 11]}}, {p157: {0:[1 3 4 5 6 2], 1:[8 9 10 11 7]}}, {p158: {0:[1 3 4 6 5 2], 1:[8 9 11 10 7]}}, {p159: {0:[1 3 6 4 5 2], 1:[8 11 9 10 7]}}, {p160: {0:[1 6 3 4 5 2], 1:[11 8 9 10 7]}}, {p161: {0:[6 1 3 4 5 2], 1:[11 8 9 10 7]}}, {p162: {0:[1 3 5 4 2 6], 1:[8 10 9 7 11]}}, {p163: {0:[1 3 5 4 6 2], 1:[8 10 9 11 7]}}, {p164: {0:[1 3 5 6 4 2], 1:[8 10 11 9 7]}}, {p165: {0:[1 3 6 5 4 2], 1:[8 11 10 9 7]}}, {p166: {0:[1 6 3 5 4 2], 1:[11 8 10 9 7]}}, {p167: {0:[6 1 3 5 4 2], 1:[11 8 10 9 7]}}, {p168: {0:[1 5 3 4 2 6], 1:[10 8 9 7 11]}}, {p169: {0:[1 5 3 4 6 2], 1:[10 8 9 11 7]}}, {p170: {0:[1 5 3 6 4 2], 1:[10 8 11 9 7]}}, {p171: {0:[1 5 6 3 4 2], 1:[10 11 8 9 7]}}, {p172: {0:[1 6 5 3 4 2], 1:[11 10 8 9 7]}}, {p173: {0:[6 1 5 3 4 2], 1:[11 10 8 9 7]}}, {p174: {0:[5 1 3 4 2 6], 1:[10 8 9 7 11]}}, {p175: {0:[5 1 3 4 6 2], 1:[10 8 9 11 7]}}, {p176: {0:[5 1 3 6 4 2], 1:[10 8 11 9 7]}}, {p177: {0:[5 1 6 3 4 2], 1:[10 11 8 9 7]}}, {p178: {0:[5 6 1 3 4 2], 1:[10 11 8 9 7]}}, {p179: {0:[6 5 1 3 4 2], 1:[11 10 8 9 7]}}, {p180: {0:[1 4 3 2 5 6], 1:[9 8 7 10 11]}}, {p181: {0:[1 4 3 2 6 5], 1:[9 8 7 11 10]}}, {p182: {0:[1 4 3 6 2 5], 1:[9 8 11 7 10]}}, {p183: {0:[1 4 6 3 2 5], 1:[9 11 8 7 10]}}, {p184: {0:[1 6 4 3 2 5], 1:[11 9 8 7 10]}}, {p185: {0:[6 1 4 3 2 5], 1:[11 9 8 7 10]}}, {p186: {0:[1 4 3 5 2 6], 1:[9 8 10 7 11]}}, {p187: {0:[1 4 3 5 6 2], 1:[9 8 10 11 7]}}, {p188: {0:[1 4 3 6 5 2], 1:[9 8 11 10 7]}}, {p189: {0:[1 4 6 3 5 2], 1:[9 11 8 10 7]}}, {p190: {0:[1 6 4 3 5 2], 1:[11 9 8 10 7]}}, {p191: {0:[6 1 4 3 5 2], 1:[11 9 8 10 7]}}, {p192: {0:[1 4 5 3 2 6], 1:[9 10 8 7 11]}}, {p193: {0:[1 4 5 3 6 2], 1:[9 10 8 11 7]}}, {p194: {0:[1 4 5 6 3 2], 1:[9 10 11 8 7]}}, {p195: {0:[1 4 6 5 3 2], 1:[9 11 10 8 7]}}, {p196: {0:[1 6 4 5 3 2], 1:[11 9 10 8 7]}}, {p197: {0:[6 1 4 5 3 2], 1:[11 9 10 8 7]}}, {p198: {0:[1 5 4 3 2 6], 1:[10 9 8 7 11]}}, {p199: {0:[1 5 4 3 6 2], 1:[10 9 8 11 7]}}, {p200: {0:[1 5 4 6 3 2], 1:[10 9 11 8 7]}}, {p201: {0:[1 5 6 4 3 2], 1:[10 11 9 8 7]}}, {p202: {0:[1 6 5 4 3 2], 1:[11 10 9 8 7]}}, {p203: {0:[6 1 5 4 3 2], 1:[11 10 9 8 7]}}, {p204: {0:[5 1 4 3 2 6], 1:[10 9 8 7 11]}}, {p205: {0:[5 1 4 3 6 2], 1:[10 9 8 11 7]}}, {p206: {0:[5 1 4 6 3 2], 1:[10 9 11 8 7]}}, {p207: {0:[5 1 6 4 3 2], 1:[10 11 9 8 7]}}, {p208: {0:[5 6 1 4 3 2], 1:[10 11 9 8 7]}}, {p209: {0:[6 5 1 4 3 2], 1:[11 10 9 8 7]}}, {p210: {0:[4 1 3 2 5 6], 1:[9 8 7 10 11]}}, {p211: {0:[4 1 3 2 6 5], 1:[9 8 7 11 10]}}, {p212: {0:[4 1 3 6 2 5], 1:[9 8 11 7 10]}}, {p213: {0:[4 1 6 3 2 5], 1:[9 11 8 7 10]}}, {p214: {0:[4 6 1 3 2 5], 1:[9 11 8 7 10]}}, {p215: {0:[6 4 1 3 2 5], 1:[11 9 8 7 10]}}, {p216: {0:[4 1 3 5 2 6], 1:[9 8 10 7 11]}}, {p217: {0:[4 1 3 5 6 2], 1:[9 8 10 11 7]}}, {p218: {0:[4 1 3 6 5 2], 1:[9 8 11 10 7]}}, {p219: {0:[4 1 6 3 5 2], 1:[9 11 8 10 7]}}, {p220: {0:[4 6 1 3 5 2], 1:[9 11 8 10 7]}}, {p221: {0:[6 4 1 3 5 2], 1:[11 9 8 10 7]}}, {p222: {0:[4 1 5 3 2 6], 1:[9 10 8 7 11]}}, {p223: {0:[4 1 5 3 6 2], 1:[9 10 8 11 7]}}, {p224: {0:[4 1 5 6 3 2], 1:[9 10 11 8 7]}}, {p225: {0:[4 1 6 5 3 2], 1:[9 11 10 8 7]}}, {p226: {0:[4 6 1 5 3 2], 1:[9 11 10 8 7]}}, {p227: {0:[6 4 1 5 3 2], 1:[11 9 10 8 7]}}, {p228: {0:[4 5 1 3 2 6], 1:[9 10 8 7 11]}}, {p229: {0:[4 5 1 3 6 2], 1:[9 10 8 11 7]}}, {p230: {0:[4 5 1 6 3 2], 1:[9 10 11 8 7]}}, {p231: {0:[4 5 6 1 3 2], 1:[9 10 11 8 7]}}, {p232: {0:[4 6 5 1 3 2], 1:[9 11 10 8 7]}}, {p233: {0:[6 4 5 1 3 2], 1:[11 9 10 8 7]}}, {p234: {0:[5 4 1 3 2 6], 1:[10 9 8 7 11]}}, {p235: {0:[5 4 1 3 6 2], 1:[10 9 8 11 7]}}, {p236: {0:[5 4 1 6 3 2], 1:[10 9 11 8 7]}}, {p237: {0:[5 4 6 1 3 2], 1:[10 9 11 8 7]}}, {p238: {0:[5 6 4 1 3 2], 1:[10 11 9 8 7]}}, {p239: {0:[6 5 4 1 3 2], 1:[11 10 9 8 7]}}, {p240: {0:[3 1 2 4 5 6], 1:[8 7 9 10 11]}}, {p241: {0:[3 1 2 4 6 5], 1:[8 7 9 11 10]}}, {p242: {0:[3 1 2 6 4 5], 1:[8 7 11 9 10]}}, {p243: {0:[3 1 6 2 4 5], 1:[8 11 7 9 10]}}, {p244: {0:[3 6 1 2 4 5], 1:[8 11 7 9 10]}}, {p245: {0:[6 3 1 2 4 5], 1:[11 8 7 9 10]}}, {p246: {0:[3 1 2 5 4 6], 1:[8 7 10 9 11]}}, {p247: {0:[3 1 2 5 6 4], 1:[8 7 10 11 9]}}, {p248: {0:[3 1 2 6 5 4], 1:[8 7 11 10 9]}}, {p249: {0:[3 1 6 2 5 4], 1:[8 11 7 10 9]}}, {p250: {0:[3 6 1 2 5 4], 1:[8 11 7 10 9]}}, {p251: {0:[6 3 1 2 5 4], 1:[11 8 7 10 9]}}, {p252: {0:[3 1 5 2 4 6], 1:[8 10 7 9 11]}}, {p253: {0:[3 1 5 2 6 4], 1:[8 10 7 11 9]}}, {p254: {0:[3 1 5 6 2 4], 1:[8 10 11 7 9]}}, {p255: {0:[3 1 6 5 2 4], 1:[8 11 10 7 9]}}, {p256: {0:[3 6 1 5 2 4], 1:[8 11 10 7 9]}}, {p257: {0:[6 3 1 5 2 4], 1:[11 8 10 7 9]}}, {p258: {0:[3 5 1 2 4 6], 1:[8 10 7 9 11]}}, {p259: {0:[3 5 1 2 6 4], 1:[8 10 7 11 9]}}, {p260: {0:[3 5 1 6 2 4], 1:[8 10 11 7 9]}}, {p261: {0:[3 5 6 1 2 4], 1:[8 10 11 7 9]}}, {p262: {0:[3 6 5 1 2 4], 1:[8 11 10 7 9]}}, {p263: {0:[6 3 5 1 2 4], 1:[11 8 10 7 9]}}, {p264: {0:[5 3 1 2 4 6], 1:[10 8 7 9 11]}}, {p265: {0:[5 3 1 2 6 4], 1:[10 8 7 11 9]}}, {p266: {0:[5 3 1 6 2 4], 1:[10 8 11 7 9]}}, {p267: {0:[5 3 6 1 2 4], 1:[10 8 11 7 9]}}, {p268: {0:[5 6 3 1 2 4], 1:[10 11 8 7 9]}}, {p269: {0:[6 5 3 1 2 4], 1:[11 10 8 7 9]}}, {p270: {0:[3 1 4 2 5 6], 1:[8 9 7 10 11]}}, {p271: {0:[3 1 4 2 6 5], 1:[8 9 7 11 10]}}, {p272: {0:[3 1 4 6 2 5], 1:[8 9 11 7 10]}}, {p273: {0:[3 1 6 4 2 5], 1:[8 11 9 7 10]}}, {p274: {0:[3 6 1 4 2 5], 1:[8 11 9 7 10]}}, {p275: {0:[6 3 1 4 2 5], 1:[11 8 9 7 10]}}, {p276: {0:[3 1 4 5 2 6], 1:[8 9 10 7 11]}}, {p277: {0:[3 1 4 5 6 2], 1:[8 9 10 11 7]}}, {p278: {0:[3 1 4 6 5 2], 1:[8 9 11 10 7]}}, {p279: {0:[3 1 6 4 5 2], 1:[8 11 9 10 7]}}, {p280: {0:[3 6 1 4 5 2], 1:[8 11 9 10 7]}}, {p281: {0:[6 3 1 4 5 2], 1:[11 8 9 10 7]}}, {p282: {0:[3 1 5 4 2 6], 1:[8 10 9 7 11]}}, {p283: {0:[3 1 5 4 6 2], 1:[8 10 9 11 7]}}, {p284: {0:[3 1 5 6 4 2], 1:[8 10 11 9 7]}}, {p285: {0:[3 1 6 5 4 2], 1:[8 11 10 9 7]}}, {p286: {0:[3 6 1 5 4 2], 1:[8 11 10 9 7]}}, {p287: {0:[6 3 1 5 4 2], 1:[11 8 10 9 7]}}, {p288: {0:[3 5 1 4 2 6], 1:[8 10 9 7 11]}}, {p289: {0:[3 5 1 4 6 2], 1:[8 10 9 11 7]}}, {p290: {0:[3 5 1 6 4 2], 1:[8 10 11 9 7]}}, {p291: {0:[3 5 6 1 4 2], 1:[8 10 11 9 7]}}, {p292: {0:[3 6 5 1 4 2], 1:[8 11 10 9 7]}}, {p293: {0:[6 3 5 1 4 2], 1:[11 8 10 9 7]}}, {p294: {0:[5 3 1 4 2 6], 1:[10 8 9 7 11]}}, {p295: {0:[5 3 1 4 6 2], 1:[10 8 9 11 7]}}, {p296: {0:[5 3 1 6 4 2], 1:[10 8 11 9 7]}}, {p297: {0:[5 3 6 1 4 2], 1:[10 8 11 9 7]}}, {p298: {0:[5 6 3 1 4 2], 1:[10 11 8 9 7]}}, {p299: {0:[6 5 3 1 4 2], 1:[11 10 8 9 7]}}, {p300: {0:[3 4 1 2 5 6], 1:[8 9 7 10 11]}}, {p301: {0:[3 4 1 2 6 5], 1:[8 9 7 11 10]}}, {p302: {0:[3 4 1 6 2 5], 1:[8 9 11 7 10]}}, {p303: {0:[3 4 6 1 2 5], 1:[8 9 11 7 10]}}, {p304: {0:[3 6 4 1 2 5], 1:[8 11 9 7 10]}}, {p305: {0:[6 3 4 1 2 5], 1:[11 8 9 7 10]}}, {p306: {0:[3 4 1 5 2 6], 1:[8 9 10 7 11]}}, {p307: {0:[3 4 1 5 6 2], 1:[8 9 10 11 7]}}, {p308: {0:[3 4 1 6 5 2], 1:[8 9 11 10 7]}}, {p309: {0:[3 4 6 1 5 2], 1:[8 9 11 10 7]}}, {p310: {0:[3 6 4 1 5 2], 1:[8 11 9 10 7]}}, {p311: {0:[6 3 4 1 5 2], 1:[11 8 9 10 7]}}, {p312: {0:[3 4 5 1 2 6], 1:[8 9 10 7 11]}}, {p313: {0:[3 4 5 1 6 2], 1:[8 9 10 11 7]}}, {p314: {0:[3 4 5 6 1 2], 1:[8 9 10 11 7]}}, {p315: {0:[3 4 6 5 1 2], 1:[8 9 11 10 7]}}, {p316: {0:[3 6 4 5 1 2], 1:[8 11 9 10 7]}}, {p317: {0:[6 3 4 5 1 2], 1:[11 8 9 10 7]}}, {p318: {0:[3 5 4 1 2 6], 1:[8 10 9 7 11]}}, {p319: {0:[3 5 4 1 6 2], 1:[8 10 9 11 7]}}, {p320: {0:[3 5 4 6 1 2], 1:[8 10 9 11 7]}}, {p321: {0:[3 5 6 4 1 2], 1:[8 10 11 9 7]}}, {p322: {0:[3 6 5 4 1 2], 1:[8 11 10 9 7]}}, {p323: {0:[6 3 5 4 1 2], 1:[11 8 10 9 7]}}, {p324: {0:[5 3 4 1 2 6], 1:[10 8 9 7 11]}}, {p325: {0:[5 3 4 1 6 2], 1:[10 8 9 11 7]}}, {p326: {0:[5 3 4 6 1 2], 1:[10 8 9 11 7]}}, {p327: {0:[5 3 6 4 1 2], 1:[10 8 11 9 7]}}, {p328: {0:[5 6 3 4 1 2], 1:[10 11 8 9 7]}}, {p329: {0:[6 5 3 4 1 2], 1:[11 10 8 9 7]}}, {p330: {0:[4 3 1 2 5 6], 1:[9 8 7 10 11]}}, {p331: {0:[4 3 1 2 6 5], 1:[9 8 7 11 10]}}, {p332: {0:[4 3 1 6 2 5], 1:[9 8 11 7 10]}}, {p333: {0:[4 3 6 1 2 5], 1:[9 8 11 7 10]}}, {p334: {0:[4 6 3 1 2 5], 1:[9 11 8 7 10]}}, {p335: {0:[6 4 3 1 2 5], 1:[11 9 8 7 10]}}, {p336: {0:[4 3 1 5 2 6], 1:[9 8 10 7 11]}}, {p337: {0:[4 3 1 5 6 2], 1:[9 8 10 11 7]}}, {p338: {0:[4 3 1 6 5 2], 1:[9 8 11 10 7]}}, {p339: {0:[4 3 6 1 5 2], 1:[9 8 11 10 7]}}, {p340: {0:[4 6 3 1 5 2], 1:[9 11 8 10 7]}}, {p341: {0:[6 4 3 1 5 2], 1:[11 9 8 10 7]}}, {p342: {0:[4 3 5 1 2 6], 1:[9 8 10 7 11]}}, {p343: {0:[4 3 5 1 6 2], 1:[9 8 10 11 7]}}, {p344: {0:[4 3 5 6 1 2], 1:[9 8 10 11 7]}}, {p345: {0:[4 3 6 5 1 2], 1:[9 8 11 10 7]}}, {p346: {0:[4 6 3 5 1 2], 1:[9 11 8 10 7]}}, {p347: {0:[6 4 3 5 1 2], 1:[11 9 8 10 7]}}, {p348: {0:[4 5 3 1 2 6], 1:[9 10 8 7 11]}}, {p349: {0:[4 5 3 1 6 2], 1:[9 10 8 11 7]}}, {p350: {0:[4 5 3 6 1 2], 1:[9 10 8 11 7]}}, {p351: {0:[4 5 6 3 1 2], 1:[9 10 11 8 7]}}, {p352: {0:[4 6 5 3 1 2], 1:[9 11 10 8 7]}}, {p353: {0:[6 4 5 3 1 2], 1:[11 9 10 8 7]}}, {p354: {0:[5 4 3 1 2 6], 1:[10 9 8 7 11]}}, {p355: {0:[5 4 3 1 6 2], 1:[10 9 8 11 7]}}, {p356: {0:[5 4 3 6 1 2], 1:[10 9 8 11 7]}}, {p357: {0:[5 4 6 3 1 2], 1:[10 9 11 8 7]}}, {p358: {0:[5 6 4 3 1 2], 1:[10 11 9 8 7]}}, {p359: {0:[6 5 4 3 1 2], 1:[11 10 9 8 7]}}, {p360: {0:[2 1 3 4 5 6], 1:[7 8 9 10 11]}}, {p361: {0:[2 1 3 4 6 5], 1:[7 8 9 11 10]}}, {p362: {0:[2 1 3 6 4 5], 1:[7 8 11 9 10]}}, {p363: {0:[2 1 6 3 4 5], 1:[7 11 8 9 10]}}, {p364: {0:[2 6 1 3 4 5], 1:[7 11 8 9 10]}}, {p365: {0:[6 2 1 3 4 5], 1:[11 7 8 9 10]}}, {p366: {0:[2 1 3 5 4 6], 1:[7 8 10 9 11]}}, {p367: {0:[2 1 3 5 6 4], 1:[7 8 10 11 9]}}, {p368: {0:[2 1 3 6 5 4], 1:[7 8 11 10 9]}}, {p369: {0:[2 1 6 3 5 4], 1:[7 11 8 10 9]}}, {p370: {0:[2 6 1 3 5 4], 1:[7 11 8 10 9]}}, {p371: {0:[6 2 1 3 5 4], 1:[11 7 8 10 9]}}, {p372: {0:[2 1 5 3 4 6], 1:[7 10 8 9 11]}}, {p373: {0:[2 1 5 3 6 4], 1:[7 10 8 11 9]}}, {p374: {0:[2 1 5 6 3 4], 1:[7 10 11 8 9]}}, {p375: {0:[2 1 6 5 3 4], 1:[7 11 10 8 9]}}, {p376: {0:[2 6 1 5 3 4], 1:[7 11 10 8 9]}}, {p377: {0:[6 2 1 5 3 4], 1:[11 7 10 8 9]}}, {p378: {0:[2 5 1 3 4 6], 1:[7 10 8 9 11]}}, {p379: {0:[2 5 1 3 6 4], 1:[7 10 8 11 9]}}, {p380: {0:[2 5 1 6 3 4], 1:[7 10 11 8 9]}}, {p381: {0:[2 5 6 1 3 4], 1:[7 10 11 8 9]}}, {p382: {0:[2 6 5 1 3 4], 1:[7 11 10 8 9]}}, {p383: {0:[6 2 5 1 3 4], 1:[11 7 10 8 9]}}, {p384: {0:[5 2 1 3 4 6], 1:[10 7 8 9 11]}}, {p385: {0:[5 2 1 3 6 4], 1:[10 7 8 11 9]}}, {p386: {0:[5 2 1 6 3 4], 1:[10 7 11 8 9]}}, {p387: {0:[5 2 6 1 3 4], 1:[10 7 11 8 9]}}, {p388: {0:[5 6 2 1 3 4], 1:[10 11 7 8 9]}}, {p389: {0:[6 5 2 1 3 4], 1:[11 10 7 8 9]}}, {p390: {0:[2 1 4 3 5 6], 1:[7 9 8 10 11]}}, {p391: {0:[2 1 4 3 6 5], 1:[7 9 8 11 10]}}, {p392: {0:[2 1 4 6 3 5], 1:[7 9 11 8 10]}}, {p393: {0:[2 1 6 4 3 5], 1:[7 11 9 8 10]}}, {p394: {0:[2 6 1 4 3 5], 1:[7 11 9 8 10]}}, {p395: {0:[6 2 1 4 3 5], 1:[11 7 9 8 10]}}, {p396: {0:[2 1 4 5 3 6], 1:[7 9 10 8 11]}}, {p397: {0:[2 1 4 5 6 3], 1:[7 9 10 11 8]}}, {p398: {0:[2 1 4 6 5 3], 1:[7 9 11 10 8]}}, {p399: {0:[2 1 6 4 5 3], 1:[7 11 9 10 8]}}, {p400: {0:[2 6 1 4 5 3], 1:[7 11 9 10 8]}}, {p401: {0:[6 2 1 4 5 3], 1:[11 7 9 10 8]}}, {p402: {0:[2 1 5 4 3 6], 1:[7 10 9 8 11]}}, {p403: {0:[2 1 5 4 6 3], 1:[7 10 9 11 8]}}, {p404: {0:[2 1 5 6 4 3], 1:[7 10 11 9 8]}}, {p405: {0:[2 1 6 5 4 3], 1:[7 11 10 9 8]}}, {p406: {0:[2 6 1 5 4 3], 1:[7 11 10 9 8]}}, {p407: {0:[6 2 1 5 4 3], 1:[11 7 10 9 8]}}, {p408: {0:[2 5 1 4 3 6], 1:[7 10 9 8 11]}}, {p409: {0:[2 5 1 4 6 3], 1:[7 10 9 11 8]}}, {p410: {0:[2 5 1 6 4 3], 1:[7 10 11 9 8]}}, {p411: {0:[2 5 6 1 4 3], 1:[7 10 11 9 8]}}, {p412: {0:[2 6 5 1 4 3], 1:[7 11 10 9 8]}}, {p413: {0:[6 2 5 1 4 3], 1:[11 7 10 9 8]}}, {p414: {0:[5 2 1 4 3 6], 1:[10 7 9 8 11]}}, {p415: {0:[5 2 1 4 6 3], 1:[10 7 9 11 8]}}, {p416: {0:[5 2 1 6 4 3], 1:[10 7 11 9 8]}}, {p417: {0:[5 2 6 1 4 3], 1:[10 7 11 9 8]}}, {p418: {0:[5 6 2 1 4 3], 1:[10 11 7 9 8]}}, {p419: {0:[6 5 2 1 4 3], 1:[11 10 7 9 8]}}, {p420: {0:[2 4 1 3 5 6], 1:[7 9 8 10 11]}}, {p421: {0:[2 4 1 3 6 5], 1:[7 9 8 11 10]}}, {p422: {0:[2 4 1 6 3 5], 1:[7 9 11 8 10]}}, {p423: {0:[2 4 6 1 3 5], 1:[7 9 11 8 10]}}, {p424: {0:[2 6 4 1 3 5], 1:[7 11 9 8 10]}}, {p425: {0:[6 2 4 1 3 5], 1:[11 7 9 8 10]}}, {p426: {0:[2 4 1 5 3 6], 1:[7 9 10 8 11]}}, {p427: {0:[2 4 1 5 6 3], 1:[7 9 10 11 8]}}, {p428: {0:[2 4 1 6 5 3], 1:[7 9 11 10 8]}}, {p429: {0:[2 4 6 1 5 3], 1:[7 9 11 10 8]}}, {p430: {0:[2 6 4 1 5 3], 1:[7 11 9 10 8]}}, {p431: {0:[6 2 4 1 5 3], 1:[11 7 9 10 8]}}, {p432: {0:[2 4 5 1 3 6], 1:[7 9 10 8 11]}}, {p433: {0:[2 4 5 1 6 3], 1:[7 9 10 11 8]}}, {p434: {0:[2 4 5 6 1 3], 1:[7 9 10 11 8]}}, {p435: {0:[2 4 6 5 1 3], 1:[7 9 11 10 8]}}, {p436: {0:[2 6 4 5 1 3], 1:[7 11 9 10 8]}}, {p437: {0:[6 2 4 5 1 3], 1:[11 7 9 10 8]}}, {p438: {0:[2 5 4 1 3 6], 1:[7 10 9 8 11]}}, {p439: {0:[2 5 4 1 6 3], 1:[7 10 9 11 8]}}, {p440: {0:[2 5 4 6 1 3], 1:[7 10 9 11 8]}}, {p441: {0:[2 5 6 4 1 3], 1:[7 10 11 9 8]}}, {p442: {0:[2 6 5 4 1 3], 1:[7 11 10 9 8]}}, {p443: {0:[6 2 5 4 1 3], 1:[11 7 10 9 8]}}, {p444: {0:[5 2 4 1 3 6], 1:[10 7 9 8 11]}}, {p445: {0:[5 2 4 1 6 3], 1:[10 7 9 11 8]}}, {p446: {0:[5 2 4 6 1 3], 1:[10 7 9 11 8]}}, {p447: {0:[5 2 6 4 1 3], 1:[10 7 11 9 8]}}, {p448: {0:[5 6 2 4 1 3], 1:[10 11 7 9 8]}}, {p449: {0:[6 5 2 4 1 3], 1:[11 10 7 9 8]}}, {p450: {0:[4 2 1 3 5 6], 1:[9 7 8 10 11]}}, {p451: {0:[4 2 1 3 6 5], 1:[9 7 8 11 10]}}, {p452: {0:[4 2 1 6 3 5], 1:[9 7 11 8 10]}}, {p453: {0:[4 2 6 1 3 5], 1:[9 7 11 8 10]}}, {p454: {0:[4 6 2 1 3 5], 1:[9 11 7 8 10]}}, {p455: {0:[6 4 2 1 3 5], 1:[11 9 7 8 10]}}, {p456: {0:[4 2 1 5 3 6], 1:[9 7 10 8 11]}}, {p457: {0:[4 2 1 5 6 3], 1:[9 7 10 11 8]}}, {p458: {0:[4 2 1 6 5 3], 1:[9 7 11 10 8]}}, {p459: {0:[4 2 6 1 5 3], 1:[9 7 11 10 8]}}, {p460: {0:[4 6 2 1 5 3], 1:[9 11 7 10 8]}}, {p461: {0:[6 4 2 1 5 3], 1:[11 9 7 10 8]}}, {p462: {0:[4 2 5 1 3 6], 1:[9 7 10 8 11]}}, {p463: {0:[4 2 5 1 6 3], 1:[9 7 10 11 8]}}, {p464: {0:[4 2 5 6 1 3], 1:[9 7 10 11 8]}}, {p465: {0:[4 2 6 5 1 3], 1:[9 7 11 10 8]}}, {p466: {0:[4 6 2 5 1 3], 1:[9 11 7 10 8]}}, {p467: {0:[6 4 2 5 1 3], 1:[11 9 7 10 8]}}, {p468: {0:[4 5 2 1 3 6], 1:[9 10 7 8 11]}}, {p469: {0:[4 5 2 1 6 3], 1:[9 10 7 11 8]}}, {p470: {0:[4 5 2 6 1 3], 1:[9 10 7 11 8]}}, {p471: {0:[4 5 6 2 1 3], 1:[9 10 11 7 8]}}, {p472: {0:[4 6 5 2 1 3], 1:[9 11 10 7 8]}}, {p473: {0:[6 4 5 2 1 3], 1:[11 9 10 7 8]}}, {p474: {0:[5 4 2 1 3 6], 1:[10 9 7 8 11]}}, {p475: {0:[5 4 2 1 6 3], 1:[10 9 7 11 8]}}, {p476: {0:[5 4 2 6 1 3], 1:[10 9 7 11 8]}}, {p477: {0:[5 4 6 2 1 3], 1:[10 9 11 7 8]}}, {p478: {0:[5 6 4 2 1 3], 1:[10 11 9 7 8]}}, {p479: {0:[6 5 4 2 1 3], 1:[11 10 9 7 8]}}, {p480: {0:[2 3 1 4 5 6], 1:[7 8 9 10 11]}}, {p481: {0:[2 3 1 4 6 5], 1:[7 8 9 11 10]}}, {p482: {0:[2 3 1 6 4 5], 1:[7 8 11 9 10]}}, {p483: {0:[2 3 6 1 4 5], 1:[7 8 11 9 10]}}, {p484: {0:[2 6 3 1 4 5], 1:[7 11 8 9 10]}}, {p485: {0:[6 2 3 1 4 5], 1:[11 7 8 9 10]}}, {p486: {0:[2 3 1 5 4 6], 1:[7 8 10 9 11]}}, {p487: {0:[2 3 1 5 6 4], 1:[7 8 10 11 9]}}, {p488: {0:[2 3 1 6 5 4], 1:[7 8 11 10 9]}}, {p489: {0:[2 3 6 1 5 4], 1:[7 8 11 10 9]}}, {p490: {0:[2 6 3 1 5 4], 1:[7 11 8 10 9]}}, {p491: {0:[6 2 3 1 5 4], 1:[11 7 8 10 9]}}, {p492: {0:[2 3 5 1 4 6], 1:[7 8 10 9 11]}}, {p493: {0:[2 3 5 1 6 4], 1:[7 8 10 11 9]}}, {p494: {0:[2 3 5 6 1 4], 1:[7 8 10 11 9]}}, {p495: {0:[2 3 6 5 1 4], 1:[7 8 11 10 9]}}, {p496: {0:[2 6 3 5 1 4], 1:[7 11 8 10 9]}}, {p497: {0:[6 2 3 5 1 4], 1:[11 7 8 10 9]}}, {p498: {0:[2 5 3 1 4 6], 1:[7 10 8 9 11]}}, {p499: {0:[2 5 3 1 6 4], 1:[7 10 8 11 9]}}, {p500: {0:[2 5 3 6 1 4], 1:[7 10 8 11 9]}}, {p501: {0:[2 5 6 3 1 4], 1:[7 10 11 8 9]}}, {p502: {0:[2 6 5 3 1 4], 1:[7 11 10 8 9]}}, {p503: {0:[6 2 5 3 1 4], 1:[11 7 10 8 9]}}, {p504: {0:[5 2 3 1 4 6], 1:[10 7 8 9 11]}}, {p505: {0:[5 2 3 1 6 4], 1:[10 7 8 11 9]}}, {p506: {0:[5 2 3 6 1 4], 1:[10 7 8 11 9]}}, {p507: {0:[5 2 6 3 1 4], 1:[10 7 11 8 9]}}, {p508: {0:[5 6 2 3 1 4], 1:[10 11 7 8 9]}}, {p509: {0:[6 5 2 3 1 4], 1:[11 10 7 8 9]}}, {p510: {0:[2 3 4 1 5 6], 1:[7 8 9 10 11]}}, {p511: {0:[2 3 4 1 6 5], 1:[7 8 9 11 10]}}, {p512: {0:[2 3 4 6 1 5], 1:[7 8 9 11 10]}}, {p513: {0:[2 3 6 4 1 5], 1:[7 8 11 9 10]}}, {p514: {0:[2 6 3 4 1 5], 1:[7 11 8 9 10]}}, {p515: {0:[6 2 3 4 1 5], 1:[11 7 8 9 10]}}, {p516: {0:[2 3 4 5 1 6], 1:[7 8 9 10 11]}}, {p517: {0:[2 3 4 5 6 1], 1:[7 8 9 10 11]}}, {p518: {0:[2 3 4 6 5 1], 1:[7 8 9 11 10]}}, {p519: {0:[2 3 6 4 5 1], 1:[7 8 11 9 10]}}, {p520: {0:[2 6 3 4 5 1], 1:[7 11 8 9 10]}}, {p521: {0:[6 2 3 4 5 1], 1:[11 7 8 9 10]}}, {p522: {0:[2 3 5 4 1 6], 1:[7 8 10 9 11]}}, {p523: {0:[2 3 5 4 6 1], 1:[7 8 10 9 11]}}, {p524: {0:[2 3 5 6 4 1], 1:[7 8 10 11 9]}}, {p525: {0:[2 3 6 5 4 1], 1:[7 8 11 10 9]}}, {p526: {0:[2 6 3 5 4 1], 1:[7 11 8 10 9]}}, {p527: {0:[6 2 3 5 4 1], 1:[11 7 8 10 9]}}, {p528: {0:[2 5 3 4 1 6], 1:[7 10 8 9 11]}}, {p529: {0:[2 5 3 4 6 1], 1:[7 10 8 9 11]}}, {p530: {0:[2 5 3 6 4 1], 1:[7 10 8 11 9]}}, {p531: {0:[2 5 6 3 4 1], 1:[7 10 11 8 9]}}, {p532: {0:[2 6 5 3 4 1], 1:[7 11 10 8 9]}}, {p533: {0:[6 2 5 3 4 1], 1:[11 7 10 8 9]}}, {p534: {0:[5 2 3 4 1 6], 1:[10 7 8 9 11]}}, {p535: {0:[5 2 3 4 6 1], 1:[10 7 8 9 11]}}, {p536: {0:[5 2 3 6 4 1], 1:[10 7 8 11 9]}}, {p537: {0:[5 2 6 3 4 1], 1:[10 7 11 8 9]}}, {p538: {0:[5 6 2 3 4 1], 1:[10 11 7 8 9]}}, {p539: {0:[6 5 2 3 4 1], 1:[11 10 7 8 9]}}, {p540: {0:[2 4 3 1 5 6], 1:[7 9 8 10 11]}}, {p541: {0:[2 4 3 1 6 5], 1:[7 9 8 11 10]}}, {p542: {0:[2 4 3 6 1 5], 1:[7 9 8 11 10]}}, {p543: {0:[2 4 6 3 1 5], 1:[7 9 11 8 10]}}, {p544: {0:[2 6 4 3 1 5], 1:[7 11 9 8 10]}}, {p545: {0:[6 2 4 3 1 5], 1:[11 7 9 8 10]}}, {p546: {0:[2 4 3 5 1 6], 1:[7 9 8 10 11]}}, {p547: {0:[2 4 3 5 6 1], 1:[7 9 8 10 11]}}, {p548: {0:[2 4 3 6 5 1], 1:[7 9 8 11 10]}}, {p549: {0:[2 4 6 3 5 1], 1:[7 9 11 8 10]}}, {p550: {0:[2 6 4 3 5 1], 1:[7 11 9 8 10]}}, {p551: {0:[6 2 4 3 5 1], 1:[11 7 9 8 10]}}, {p552: {0:[2 4 5 3 1 6], 1:[7 9 10 8 11]}}, {p553: {0:[2 4 5 3 6 1], 1:[7 9 10 8 11]}}, {p554: {0:[2 4 5 6 3 1], 1:[7 9 10 11 8]}}, {p555: {0:[2 4 6 5 3 1], 1:[7 9 11 10 8]}}, {p556: {0:[2 6 4 5 3 1], 1:[7 11 9 10 8]}}, {p557: {0:[6 2 4 5 3 1], 1:[11 7 9 10 8]}}, {p558: {0:[2 5 4 3 1 6], 1:[7 10 9 8 11]}}, {p559: {0:[2 5 4 3 6 1], 1:[7 10 9 8 11]}}, {p560: {0:[2 5 4 6 3 1], 1:[7 10 9 11 8]}}, {p561: {0:[2 5 6 4 3 1], 1:[7 10 11 9 8]}}, {p562: {0:[2 6 5 4 3 1], 1:[7 11 10 9 8]}}, {p563: {0:[6 2 5 4 3 1], 1:[11 7 10 9 8]}}, {p564: {0:[5 2 4 3 1 6], 1:[10 7 9 8 11]}}, {p565: {0:[5 2 4 3 6 1], 1:[10 7 9 8 11]}}, {p566: {0:[5 2 4 6 3 1], 1:[10 7 9 11 8]}}, {p567: {0:[5 2 6 4 3 1], 1:[10 7 11 9 8]}}, {p568: {0:[5 6 2 4 3 1], 1:[10 11 7 9 8]}}, {p569: {0:[6 5 2 4 3 1], 1:[11 10 7 9 8]}}, {p570: {0:[4 2 3 1 5 6], 1:[9 7 8 10 11]}}, {p571: {0:[4 2 3 1 6 5], 1:[9 7 8 11 10]}}, {p572: {0:[4 2 3 6 1 5], 1:[9 7 8 11 10]}}, {p573: {0:[4 2 6 3 1 5], 1:[9 7 11 8 10]}}, {p574: {0:[4 6 2 3 1 5], 1:[9 11 7 8 10]}}, {p575: {0:[6 4 2 3 1 5], 1:[11 9 7 8 10]}}, {p576: {0:[4 2 3 5 1 6], 1:[9 7 8 10 11]}}, {p577: {0:[4 2 3 5 6 1], 1:[9 7 8 10 11]}}, {p578: {0:[4 2 3 6 5 1], 1:[9 7 8 11 10]}}, {p579: {0:[4 2 6 3 5 1], 1:[9 7 11 8 10]}}, {p580: {0:[4 6 2 3 5 1], 1:[9 11 7 8 10]}}, {p581: {0:[6 4 2 3 5 1], 1:[11 9 7 8 10]}}, {p582: {0:[4 2 5 3 1 6], 1:[9 7 10 8 11]}}, {p583: {0:[4 2 5 3 6 1], 1:[9 7 10 8 11]}}, {p584: {0:[4 2 5 6 3 1], 1:[9 7 10 11 8]}}, {p585: {0:[4 2 6 5 3 1], 1:[9 7 11 10 8]}}, {p586: {0:[4 6 2 5 3 1], 1:[9 11 7 10 8]}}, {p587: {0:[6 4 2 5 3 1], 1:[11 9 7 10 8]}}, {p588: {0:[4 5 2 3 1 6], 1:[9 10 7 8 11]}}, {p589: {0:[4 5 2 3 6 1], 1:[9 10 7 8 11]}}, {p590: {0:[4 5 2 6 3 1], 1:[9 10 7 11 8]}}, {p591: {0:[4 5 6 2 3 1], 1:[9 10 11 7 8]}}, {p592: {0:[4 6 5 2 3 1], 1:[9 11 10 7 8]}}, {p593: {0:[6 4 5 2 3 1], 1:[11 9 10 7 8]}}, {p594: {0:[5 4 2 3 1 6], 1:[10 9 7 8 11]}}, {p595: {0:[5 4 2 3 6 1], 1:[10 9 7 8 11]}}, {p596: {0:[5 4 2 6 3 1], 1:[10 9 7 11 8]}}, {p597: {0:[5 4 6 2 3 1], 1:[10 9 11 7 8]}}, {p598: {0:[5 6 4 2 3 1], 1:[10 11 9 7 8]}}, {p599: {0:[6 5 4 2 3 1], 1:[11 10 9 7 8]}}, {p600: {0:[3 2 1 4 5 6], 1:[8 7 9 10 11]}}, {p601: {0:[3 2 1 4 6 5], 1:[8 7 9 11 10]}}, {p602: {0:[3 2 1 6 4 5], 1:[8 7 11 9 10]}}, {p603: {0:[3 2 6 1 4 5], 1:[8 7 11 9 10]}}, {p604: {0:[3 6 2 1 4 5], 1:[8 11 7 9 10]}}, {p605: {0:[6 3 2 1 4 5], 1:[11 8 7 9 10]}}, {p606: {0:[3 2 1 5 4 6], 1:[8 7 10 9 11]}}, {p607: {0:[3 2 1 5 6 4], 1:[8 7 10 11 9]}}, {p608: {0:[3 2 1 6 5 4], 1:[8 7 11 10 9]}}, {p609: {0:[3 2 6 1 5 4], 1:[8 7 11 10 9]}}, {p610: {0:[3 6 2 1 5 4], 1:[8 11 7 10 9]}}, {p611: {0:[6 3 2 1 5 4], 1:[11 8 7 10 9]}}, {p612: {0:[3 2 5 1 4 6], 1:[8 7 10 9 11]}}, {p613: {0:[3 2 5 1 6 4], 1:[8 7 10 11 9]}}, {p614: {0:[3 2 5 6 1 4], 1:[8 7 10 11 9]}}, {p615: {0:[3 2 6 5 1 4], 1:[8 7 11 10 9]}}, {p616: {0:[3 6 2 5 1 4], 1:[8 11 7 10 9]}}, {p617: {0:[6 3 2 5 1 4], 1:[11 8 7 10 9]}}, {p618: {0:[3 5 2 1 4 6], 1:[8 10 7 9 11]}}, {p619: {0:[3 5 2 1 6 4], 1:[8 10 7 11 9]}}, {p620: {0:[3 5 2 6 1 4], 1:[8 10 7 11 9]}}, {p621: {0:[3 5 6 2 1 4], 1:[8 10 11 7 9]}}, {p622: {0:[3 6 5 2 1 4], 1:[8 11 10 7 9]}}, {p623: {0:[6 3 5 2 1 4], 1:[11 8 10 7 9]}}, {p624: {0:[5 3 2 1 4 6], 1:[10 8 7 9 11]}}, {p625: {0:[5 3 2 1 6 4], 1:[10 8 7 11 9]}}, {p626: {0:[5 3 2 6 1 4], 1:[10 8 7 11 9]}}, {p627: {0:[5 3 6 2 1 4], 1:[10 8 11 7 9]}}, {p628: {0:[5 6 3 2 1 4], 1:[10 11 8 7 9]}}, {p629: {0:[6 5 3 2 1 4], 1:[11 10 8 7 9]}}, {p630: {0:[3 2 4 1 5 6], 1:[8 7 9 10 11]}}, {p631: {0:[3 2 4 1 6 5], 1:[8 7 9 11 10]}}, {p632: {0:[3 2 4 6 1 5], 1:[8 7 9 11 10]}}, {p633: {0:[3 2 6 4 1 5], 1:[8 7 11 9 10]}}, {p634: {0:[3 6 2 4 1 5], 1:[8 11 7 9 10]}}, {p635: {0:[6 3 2 4 1 5], 1:[11 8 7 9 10]}}, {p636: {0:[3 2 4 5 1 6], 1:[8 7 9 10 11]}}, {p637: {0:[3 2 4 5 6 1], 1:[8 7 9 10 11]}}, {p638: {0:[3 2 4 6 5 1], 1:[8 7 9 11 10]}}, {p639: {0:[3 2 6 4 5 1], 1:[8 7 11 9 10]}}, {p640: {0:[3 6 2 4 5 1], 1:[8 11 7 9 10]}}, {p641: {0:[6 3 2 4 5 1], 1:[11 8 7 9 10]}}, {p642: {0:[3 2 5 4 1 6], 1:[8 7 10 9 11]}}, {p643: {0:[3 2 5 4 6 1], 1:[8 7 10 9 11]}}, {p644: {0:[3 2 5 6 4 1], 1:[8 7 10 11 9]}}, {p645: {0:[3 2 6 5 4 1], 1:[8 7 11 10 9]}}, {p646: {0:[3 6 2 5 4 1], 1:[8 11 7 10 9]}}, {p647: {0:[6 3 2 5 4 1], 1:[11 8 7 10 9]}}, {p648: {0:[3 5 2 4 1 6], 1:[8 10 7 9 11]}}, {p649: {0:[3 5 2 4 6 1], 1:[8 10 7 9 11]}}, {p650: {0:[3 5 2 6 4 1], 1:[8 10 7 11 9]}}, {p651: {0:[3 5 6 2 4 1], 1:[8 10 11 7 9]}}, {p652: {0:[3 6 5 2 4 1], 1:[8 11 10 7 9]}}, {p653: {0:[6 3 5 2 4 1], 1:[11 8 10 7 9]}}, {p654: {0:[5 3 2 4 1 6], 1:[10 8 7 9 11]}}, {p655: {0:[5 3 2 4 6 1], 1:[10 8 7 9 11]}}, {p656: {0:[5 3 2 6 4 1], 1:[10 8 7 11 9]}}, {p657: {0:[5 3 6 2 4 1], 1:[10 8 11 7 9]}}, {p658: {0:[5 6 3 2 4 1], 1:[10 11 8 7 9]}}, {p659: {0:[6 5 3 2 4 1], 1:[11 10 8 7 9]}}, {p660: {0:[3 4 2 1 5 6], 1:[8 9 7 10 11]}}, {p661: {0:[3 4 2 1 6 5], 1:[8 9 7 11 10]}}, {p662: {0:[3 4 2 6 1 5], 1:[8 9 7 11 10]}}, {p663: {0:[3 4 6 2 1 5], 1:[8 9 11 7 10]}}, {p664: {0:[3 6 4 2 1 5], 1:[8 11 9 7 10]}}, {p665: {0:[6 3 4 2 1 5], 1:[11 8 9 7 10]}}, {p666: {0:[3 4 2 5 1 6], 1:[8 9 7 10 11]}}, {p667: {0:[3 4 2 5 6 1], 1:[8 9 7 10 11]}}, {p668: {0:[3 4 2 6 5 1], 1:[8 9 7 11 10]}}, {p669: {0:[3 4 6 2 5 1], 1:[8 9 11 7 10]}}, {p670: {0:[3 6 4 2 5 1], 1:[8 11 9 7 10]}}, {p671: {0:[6 3 4 2 5 1], 1:[11 8 9 7 10]}}, {p672: {0:[3 4 5 2 1 6], 1:[8 9 10 7 11]}}, {p673: {0:[3 4 5 2 6 1], 1:[8 9 10 7 11]}}, {p674: {0:[3 4 5 6 2 1], 1:[8 9 10 11 7]}}, {p675: {0:[3 4 6 5 2 1], 1:[8 9 11 10 7]}}, {p676: {0:[3 6 4 5 2 1], 1:[8 11 9 10 7]}}, {p677: {0:[6 3 4 5 2 1], 1:[11 8 9 10 7]}}, {p678: {0:[3 5 4 2 1 6], 1:[8 10 9 7 11]}}, {p679: {0:[3 5 4 2 6 1], 1:[8 10 9 7 11]}}, {p680: {0:[3 5 4 6 2 1], 1:[8 10 9 11 7]}}, {p681: {0:[3 5 6 4 2 1], 1:[8 10 11 9 7]}}, {p682: {0:[3 6 5 4 2 1], 1:[8 11 10 9 7]}}, {p683: {0:[6 3 5 4 2 1], 1:[11 8 10 9 7]}}, {p684: {0:[5 3 4 2 1 6], 1:[10 8 9 7 11]}}, {p685: {0:[5 3 4 2 6 1], 1:[10 8 9 7 11]}}, {p686: {0:[5 3 4 6 2 1], 1:[10 8 9 11 7]}}, {p687: {0:[5 3 6 4 2 1], 1:[10 8 11 9 7]}}, {p688: {0:[5 6 3 4 2 1], 1:[10 11 8 9 7]}}, {p689: {0:[6 5 3 4 2 1], 1:[11 10 8 9 7]}}, {p690: {0:[4 3 2 1 5 6], 1:[9 8 7 10 11]}}, {p691: {0:[4 3 2 1 6 5], 1:[9 8 7 11 10]}}, {p692: {0:[4 3 2 6 1 5], 1:[9 8 7 11 10]}}, {p693: {0:[4 3 6 2 1 5], 1:[9 8 11 7 10]}}, {p694: {0:[4 6 3 2 1 5], 1:[9 11 8 7 10]}}, {p695: {0:[6 4 3 2 1 5], 1:[11 9 8 7 10]}}, {p696: {0:[4 3 2 5 1 6], 1:[9 8 7 10 11]}}, {p697: {0:[4 3 2 5 6 1], 1:[9 8 7 10 11]}}, {p698: {0:[4 3 2 6 5 1], 1:[9 8 7 11 10]}}, {p699: {0:[4 3 6 2 5 1], 1:[9 8 11 7 10]}}, {p700: {0:[4 6 3 2 5 1], 1:[9 11 8 7 10]}}, {p701: {0:[6 4 3 2 5 1], 1:[11 9 8 7 10]}}, {p702: {0:[4 3 5 2 1 6], 1:[9 8 10 7 11]}}, {p703: {0:[4 3 5 2 6 1], 1:[9 8 10 7 11]}}, {p704: {0:[4 3 5 6 2 1], 1:[9 8 10 11 7]}}, {p705: {0:[4 3 6 5 2 1], 1:[9 8 11 10 7]}}, {p706: {0:[4 6 3 5 2 1], 1:[9 11 8 10 7]}}, {p707: {0:[6 4 3 5 2 1], 1:[11 9 8 10 7]}}, {p708: {0:[4 5 3 2 1 6], 1:[9 10 8 7 11]}}, {p709: {0:[4 5 3 2 6 1], 1:[9 10 8 7 11]}}, {p710: {0:[4 5 3 6 2 1], 1:[9 10 8 11 7]}}, {p711: {0:[4 5 6 3 2 1], 1:[9 10 11 8 7]}}, {p712: {0:[4 6 5 3 2 1], 1:[9 11 10 8 7]}}, {p713: {0:[6 4 5 3 2 1], 1:[11 9 10 8 7]}}, {p714: {0:[5 4 3 2 1 6], 1:[10 9 8 7 11]}}, {p715: {0:[5 4 3 2 6 1], 1:[10 9 8 7 11]}}, {p716: {0:[5 4 3 6 2 1], 1:[10 9 8 11 7]}}, {p717: {0:[5 4 6 3 2 1], 1:[10 9 11 8 7]}}, {p718: {0:[5 6 4 3 2 1], 1:[10 11 9 8 7]}}, {p719: {0:[6 5 4 3 2 1], 1:[11 10 9 8 7]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}, {Id:2 RootId:2 Layer:0 X:72.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[7]}, {Id:3 RootId:3 Layer:0 X:144.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[8]}, {Id:4 RootId:4 Layer:0 X:216.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[9]}, {Id:5 RootId:5 Layer:0 X:288.00 Y:0.00 TotalW:64.00 W:52.00 H:60.00 In:[] PriRootOut:[10]}, {Id:6 RootId:6 Layer:0 X:372.00 Y:0.00 TotalW:64.00 W:52.00 H:60.00 In:[] PriRootOut:[11]}, {Id:7 RootId:2 Layer:1 X:72.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:8 RootId:3 Layer:1 X:144.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:9 RootId:4 Layer:1 X:216.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:10 RootId:5 Layer:1 X:288.00 Y:276.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:11 RootId:6 Layer:1 X:372.00 Y:276.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:384.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[]}, {Id:2 RootId:2 Layer:0 X:312.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[7]}, {Id:3 RootId:3 Layer:0 X:240.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[8]}, {Id:4 RootId:4 Layer:0 X:168.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[9]}, {Id:5 RootId:5 Layer:0 X:84.00 Y:0.00 TotalW:64.00 W:52.00 H:60.00 In:[] PriRootOut:[10]}, {Id:6 RootId:6 Layer:0 X:0.00 Y:0.00 TotalW:64.00 W:52.00 H:60.00 In:[] PriRootOut:[11]}, {Id:7 RootId:2 Layer:1 X:312.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:8 RootId:3 Layer:1 X:240.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:9 RootId:4 Layer:1 X:168.00 Y:276.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:10 RootId:5 Layer:1 X:84.00 Y:276.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}, {Id:11 RootId:6 Layer:1 X:0.00 Y:276.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:1 X:0.00 Y:0.00 W:37.08 H:36.00}] PriRootOut:[]}") +} + +func TestLayerLongRootsMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsLayerLongRoots, + "[-4 2 3 4 5 0 1 2 3 3 4 5 6]", + "0:[5], 1:[6], 2:[1 7], 3:[2 8 9], 4:[3 10], 5:[4 11], 6:[12]", + int64(6), + "{p0: {0:[5], 1:[6], 2:[7 1], 3:[8 2 9], 4:[3 10], 5:[4 11], 6:[12]}}, {p1: {0:[5], 1:[6], 2:[7 1], 3:[8 9 2], 4:[10 3], 5:[11 4], 6:[12]}}, {p2: {0:[5], 1:[6], 2:[7 1], 3:[9 8 2], 4:[10 3], 5:[11 4], 6:[12]}}, {p3: {0:[5], 1:[6], 2:[1 7], 3:[2 8 9], 4:[3 10], 5:[4 11], 6:[12]}}, {p4: {0:[5], 1:[6], 2:[1 7], 3:[2 9 8], 4:[3 10], 5:[4 11], 6:[12]}}, {p5: {0:[5], 1:[6], 2:[1 7], 3:[9 2 8], 4:[10 3], 5:[11 4], 6:[12]}}", + "{Id:1 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:5 X:72.00 Y:600.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:5 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[6]}, {Id:6 RootId:5 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:5 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[8]}, {Id:8 RootId:5 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:7 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:9 RootId:9 Layer:3 X:144.00 Y:360.00 TotalW:64.00 W:52.00 H:60.00 In:[] PriRootOut:[10]}, {Id:10 RootId:9 Layer:4 X:144.00 Y:480.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:9 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[11]}, {Id:11 RootId:9 Layer:5 X:144.00 Y:600.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:10 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[12]}, {Id:12 RootId:9 Layer:6 X:144.00 Y:720.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:11 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:2 X:72.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3]}, {Id:3 RootId:1 Layer:4 X:72.00 Y:480.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:8 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4]}, {Id:4 RootId:1 Layer:5 X:72.00 Y:600.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:5 Layer:0 X:0.00 Y:0.00 TotalW:52.00 W:52.00 H:60.00 In:[] PriRootOut:[6]}, {Id:6 RootId:5 Layer:1 X:0.00 Y:120.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:5 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7]}, {Id:7 RootId:5 Layer:2 X:0.00 Y:240.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[8]}, {Id:8 RootId:5 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:7 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:9 RootId:9 Layer:3 X:144.00 Y:360.00 TotalW:64.00 W:52.00 H:60.00 In:[] PriRootOut:[10]}, {Id:10 RootId:9 Layer:4 X:144.00 Y:480.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:9 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[11]}, {Id:11 RootId:9 Layer:5 X:144.00 Y:600.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:10 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[12]}, {Id:12 RootId:9 Layer:6 X:144.00 Y:720.00 TotalW:64.00 W:64.00 H:60.00 In:[{HT:pri SrcId:11 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:4 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func TestPriAndSecInfinitePulldownMxPermutator(t *testing.T) { + helperAll(t, + testNodeDefsPriAndSecInfinitePulldown, + "[-4 0 1 2 3 3 2 3 3]", + "0:[1], 1:[2], 2:[3 6], 3:[4 5 7 8]", + int64(4), + "{p0: {0:[1], 1:[2], 2:[3 6], 3:[4 5 7 8]}}, {p1: {0:[1], 1:[2], 2:[3 6], 3:[4 5 8 7]}}, {p2: {0:[1], 1:[2], 2:[6 3], 3:[7 8 4 5]}}, {p3: {0:[1], 1:[2], 2:[6 3], 3:[7 8 5 4]}}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:268.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:268.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[3 6]}, {Id:3 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:124.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[4 5]}, {Id:4 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:1 Layer:2 X:144.00 Y:240.00 TotalW:124.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7 8]}, {Id:7 RootId:1 Layer:3 X:144.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:8 RootId:1 Layer:3 X:216.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}", + "{Id:1 RootId:1 Layer:0 X:0.00 Y:0.00 TotalW:268.00 W:52.00 H:60.00 In:[] PriRootOut:[2]}, {Id:2 RootId:1 Layer:1 X:0.00 Y:120.00 TotalW:268.00 W:52.00 H:60.00 In:[{HT:pri SrcId:1 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[6 3]}, {Id:3 RootId:1 Layer:2 X:144.00 Y:240.00 TotalW:124.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[5 4]}, {Id:4 RootId:1 Layer:3 X:216.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:5 RootId:1 Layer:3 X:144.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:3 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:6 RootId:1 Layer:2 X:0.00 Y:240.00 TotalW:124.00 W:52.00 H:60.00 In:[{HT:pri SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[7 8]}, {Id:7 RootId:1 Layer:3 X:0.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}, {Id:8 RootId:1 Layer:3 X:72.00 Y:360.00 TotalW:52.00 W:52.00 H:60.00 In:[{HT:pri SrcId:6 X:0.00 Y:0.00 W:0.00 H:0.00} {HT:sec SrcId:2 X:0.00 Y:0.00 W:0.00 H:0.00}] PriRootOut:[]}") +} + +func Test40milPermsMxPermutator(t *testing.T) { + // defer profile.Start(profile.CPUProfile).Stop() + helperIteratorAndIncrementalCount(t, + testNodeDefs40milPerms, + "[-4 0 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1]", + "0:[1], 1:[2 3 4 20], 2:[5 6 7 8 9 10 11 12 13 14 15 16 17 18 19]", + int64(41472000), + int64(41472000)) +} + +func Test300bilPermsMxPermutator(t *testing.T) { + layerMap := buildLayerMap(testNodeDefs300bilPerms) + assert.Equal(t, "[-4 0 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1]", fmt.Sprintf("%v", layerMap)) + + rootNodes := buildRootNodeList(buildPriParentMap(testNodeDefs300bilPerms)) + mx, _ := NewLayerMx(testNodeDefs300bilPerms, layerMap, rootNodes) + assert.Equal(t, "0:[1], 1:[2 3 4 22 23 24 25], 2:[5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21]", mx.String()) + + _, err := NewLayerMxPermIterator(testNodeDefs300bilPerms, mx) + assert.Contains(t, err.Error(), "313528320000") +} diff --git a/pkg/capigraph/layer_mx_test.go b/pkg/capigraph/layer_mx_test.go new file mode 100644 index 00000000..75a8a7ab --- /dev/null +++ b/pkg/capigraph/layer_mx_test.go @@ -0,0 +1,229 @@ +package capigraph + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// 0: 1 +// - | \ +// 1: 2 4 +// - | | +// 2: 3 5 +func TestTrivial(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + } + priParentMap := buildPriParentMap(nodeDefs) + + mx := LayerMx{ + {1}, + {2, 4}, + {3, 5}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1}, + {2, 4}, + {5, 3}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) +} + +// 0: 1 +// - | \ +// 1: 2 4 +// - | | +// 2: F3 5 +// - | / +// 3: 3 +func TestOneFake(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{{5, ""}}, "", 0, false}, + {4, "4", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + } + priParentMap := buildPriParentMap(nodeDefs) + + mx := LayerMx{ + {1}, + {2, 4}, + {10003, 5}, + {3}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1}, + {4, 2}, + {10003, 5}, + {3}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) +} + +// 0: 1 +// - | \ +// 1: 2 4 +// - | | +// 2: F3 5 +// - | | +// 3: F3 6 +// - | / +// 4: 3 +func TestTwoFake(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{{6, ""}}, "", 0, false}, + {4, "4", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{5, ""}, []EdgeDef{}, "", 0, false}, + } + priParentMap := buildPriParentMap(nodeDefs) + mx := LayerMx{ + {1}, + {2, 4}, + {10003, 5}, + {10003, 6}, + {3}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1}, + {4, 2}, + {10003, 5}, + {6, 10003}, + {3}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1}, + {2, 4}, + {5, 10003}, + {10003, 6}, + {3}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) +} + +// 0: 1 3 +// - | | +// 1: 2 4 6 +// - | / +// 2: 5 +func TestParentless(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{{6, ""}}, "", 0, false}, + {6, "6", EdgeDef{}, []EdgeDef{}, "", 0, false}, + } + priParentMap := buildPriParentMap(nodeDefs) + var mx LayerMx + + mx = LayerMx{ + {3, 1}, + {6, 4, 2}, + {5}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {3, 1}, + {4, 6, 2}, + {5}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {3, 1}, + {4, 2, 6}, + {5}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {3, 1}, + {6, 2, 4}, + {5}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {3, 1}, + {2, 6, 4}, + {5}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {3, 1}, + {2, 4, 6}, + {5}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1, 3}, + {2, 4, 6}, + {5}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1, 3}, + {6, 2, 4}, + {5}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1, 3}, + {2, 6, 4}, + {5}, + } + assert.True(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1, 3}, + {4, 2, 6}, + {5}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1, 3}, + {4, 6, 2}, + {5}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) + + mx = LayerMx{ + {1, 3}, + {6, 4, 2}, + {5}, + } + assert.False(t, mx.isMonotonous(priParentMap, len(nodeDefs))) +} + +func TestSignature(t *testing.T) { + mx := LayerMx{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 32, 64, 128, 256, 512, 1024, 1025}} + assert.Equal(t, "0000000100020003000400050006000700080009000A000B000C000D000E000F00100020004000800100020004000401", mx.signature()) +} diff --git a/pkg/capigraph/layer_permutator.go b/pkg/capigraph/layer_permutator.go new file mode 100644 index 00000000..23a18f12 --- /dev/null +++ b/pkg/capigraph/layer_permutator.go @@ -0,0 +1,250 @@ +package capigraph + +import ( + "fmt" +) + +func swap(a []int16, i1 int, i2 int) { + t := a[i2] + a[i2] = a[i1] + a[i1] = t +} + +const MaxSupportedFact int = 6 // Max amount of pri children originating from a node +const MaxNodesToInsert int = 10 // Cannot insert more than this amt of new roots at a layer +const MaxIntervalsInLayer int = 20 // Max parent nodes containing more than one child on this layer +const MaxLayerLen int = 200 + +type LayerPermutator struct { + P [][][]int // Swap permutation strategy by permIdx: [interval size 2-6][permIdx 2!-6!][positions to swap with] + Fact []int + SrcPerm []int16 + WorkPerm []int16 + IntervalStarts []int + IntervalLengths []int +} + +func NewLayerPermutator() *LayerPermutator { + lp := LayerPermutator{} + lp.init() + return &lp +} + +func (lp *LayerPermutator) insertPermutationByIdx(perm []int16, itemToInsertSrcIdx int, permIdx int) { + if permIdx == itemToInsertSrcIdx { + // Identity permutation, totally valid + return + } + if permIdx > itemToInsertSrcIdx { + panic(fmt.Sprintf("insertPermutationByIdx(): cannot move item %d from % d to %d", perm[itemToInsertSrcIdx], itemToInsertSrcIdx, permIdx)) + } + idToInsert := perm[itemToInsertSrcIdx] + // Shift + copy(perm[permIdx+1:], perm[permIdx:itemToInsertSrcIdx]) + // Set the item to be inserted + perm[permIdx] = idToInsert +} + +// Execute swap perm strategy for a specific intervalLen and permIdx + +func (lp *LayerPermutator) swapPermutationByIdx(perm []int16, firstIdx int, intervalLen int, permIdx int) { + if intervalLen > MaxSupportedFact { + panic(fmt.Sprintf("permutationByIdx(): factorial value not supported %d, max supported %d", intervalLen, MaxSupportedFact)) + } + if intervalLen == 2 { + if intervalLen-1 > lp.P[2][permIdx][0] { + swap(perm, firstIdx+intervalLen-1, firstIdx+lp.P[2][permIdx][0]) + } + } else if intervalLen == 3 { + if intervalLen-2 > lp.P[3][permIdx][0] { + swap(perm, firstIdx+intervalLen-2, firstIdx+lp.P[3][permIdx][0]) + } + if intervalLen-1 > lp.P[3][permIdx][1] { + swap(perm, firstIdx+intervalLen-1, firstIdx+lp.P[3][permIdx][1]) + } + } else if intervalLen == 4 { + if intervalLen-3 > lp.P[4][permIdx][0] { + swap(perm, firstIdx+intervalLen-3, firstIdx+lp.P[4][permIdx][0]) + } + if intervalLen-2 > lp.P[4][permIdx][1] { + swap(perm, firstIdx+intervalLen-2, firstIdx+lp.P[4][permIdx][1]) + } + if intervalLen-1 > lp.P[4][permIdx][2] { + swap(perm, firstIdx+intervalLen-1, firstIdx+lp.P[4][permIdx][2]) + } + } else if intervalLen == 5 { + if intervalLen-4 > lp.P[5][permIdx][0] { + swap(perm, firstIdx+intervalLen-4, firstIdx+lp.P[5][permIdx][0]) + } + if intervalLen-3 > lp.P[5][permIdx][1] { + swap(perm, firstIdx+intervalLen-3, firstIdx+lp.P[5][permIdx][1]) + } + if intervalLen-2 > lp.P[5][permIdx][2] { + swap(perm, firstIdx+intervalLen-2, firstIdx+lp.P[5][permIdx][2]) + } + if intervalLen-1 > lp.P[5][permIdx][3] { + swap(perm, firstIdx+intervalLen-1, firstIdx+lp.P[5][permIdx][3]) + } + } else if intervalLen == 6 { + if intervalLen-5 > lp.P[6][permIdx][0] { + swap(perm, firstIdx+intervalLen-5, firstIdx+lp.P[6][permIdx][0]) + } + if intervalLen-4 > lp.P[6][permIdx][1] { + swap(perm, firstIdx+intervalLen-4, firstIdx+lp.P[6][permIdx][1]) + } + if intervalLen-3 > lp.P[6][permIdx][2] { + swap(perm, firstIdx+intervalLen-3, firstIdx+lp.P[6][permIdx][2]) + } + if intervalLen-2 > lp.P[6][permIdx][3] { + swap(perm, firstIdx+intervalLen-2, firstIdx+lp.P[6][permIdx][3]) + } + if intervalLen-1 > lp.P[6][permIdx][4] { + swap(perm, firstIdx+intervalLen-1, firstIdx+lp.P[6][permIdx][4]) + } + } +} + +func (lp *LayerPermutator) init() { + lp.Fact = make([]int, MaxSupportedFact+1) + acc := 1 + for i := range MaxSupportedFact + 1 { + lp.Fact[i] = acc + acc *= (i + 1) + } + + lp.P = make([][][]int, MaxSupportedFact+1) + i := 2 + for i <= 6 { + lp.P[i] = make([][]int, lp.Fact[i]) + p := lp.P[i] + for j := range len(p) { + p[j] = make([]int, i-1) + } + + k := 0 + for k < i-1 { + streakSize := len(p) / lp.Fact[k+2] + maxStreakVal := k + 1 + curVal := 0 + curPosWithinStreak := 0 + + for j := range len(p) { + p[j][k] = curVal + curPosWithinStreak++ + if curPosWithinStreak == streakSize { + curPosWithinStreak = 0 + curVal++ + } + if curVal == maxStreakVal+1 { + curVal = 0 + } + } + k++ + } + i++ + } +} + +func (lp *LayerPermutator) initSource(in []int16) { + lp.SrcPerm = in + if lp.WorkPerm == nil { + lp.WorkPerm = make([]int16, len(lp.SrcPerm), MaxLayerLen) // Assume this capacity is enough + } else if len(lp.WorkPerm) != len(lp.SrcPerm) { + lp.WorkPerm = lp.WorkPerm[:len(lp.SrcPerm)] // Shrink, or grow, hopefully no reallocation + } + copy(lp.WorkPerm, lp.SrcPerm) +} + +func (lp *LayerPermutator) initIntervals(permIntervalStarts []int, permIntervalLengths []int, totalIntervals int) { + lp.IntervalStarts = make([]int, MaxIntervalsInLayer) + lp.IntervalLengths = make([]int, MaxIntervalsInLayer) + lp.IntervalStarts = lp.IntervalStarts[:totalIntervals] + copy(lp.IntervalStarts, permIntervalStarts[:totalIntervals]) + lp.IntervalLengths = lp.IntervalLengths[:totalIntervals] + copy(lp.IntervalLengths, permIntervalLengths[:totalIntervals]) +} + +func (lp *LayerPermutator) SwapIterator(intervalStarts []int, intervalLengths []int, totalIntervals int, in []int16, f func(int, []int16)) { + lp.initSource(in) + lp.initIntervals(intervalStarts, intervalLengths, totalIntervals) + swapFuncIteratorRecursive(lp, 0, 0, f) +} + +func swapFuncIteratorRecursive(lp *LayerPermutator, intervalIdx int, totalCnt int, f func(int, []int16)) { + cb := func(int, []int16) { + f(totalCnt, lp.WorkPerm) + totalCnt++ + } + intStart := lp.IntervalStarts[intervalIdx] + intLen := lp.IntervalLengths[intervalIdx] + if intLen > MaxSupportedFact { + panic(fmt.Sprintf("swapFuncIteratorRecursive: swap interval too big: %d, max supported %d", intLen, MaxSupportedFact)) + } + // Apply permutation backwards. The only reason for this is: + // the "original, untouched" permutation ("swap nothing") is the last one in the permutation table, + // and that is the one we want to to win all other factors equal. So it goes first. + i := lp.Fact[intLen] - 1 + for i >= 0 { + // Re-initialize slice interval we work with + copy(lp.WorkPerm[intStart:], lp.SrcPerm[intStart:intStart+intLen]) + // Swap in-place + lp.swapPermutationByIdx(lp.WorkPerm, intStart, intLen, i) + if intervalIdx == len(lp.IntervalStarts)-1 { + f(totalCnt, lp.WorkPerm) + totalCnt++ + } else { + swapFuncIteratorRecursive(lp, intervalIdx+1, totalCnt, cb) + } + i-- + } +} + +func (lp *LayerPermutator) InsertIterator(insertSrcStart int, insertSrcLen int, in []int16, f func(int, []int16)) { + lp.initSource(in) + insertFuncIteratorRecursive(lp, insertSrcStart, insertSrcLen, insertSrcStart, 0, f) +} + +func insertFuncIteratorRecursive(lp *LayerPermutator, insertSrcStart int, insertSrcLen int, curInsertSrc int, totalCnt int, f func(int, []int16)) { + if insertSrcLen > MaxNodesToInsert { + panic(fmt.Sprintf("insertFuncIteratorRecursive: too many ids to insert: %d, max supported %d", insertSrcLen, MaxNodesToInsert)) + } + cb := func(int, []int16) { + f(totalCnt, lp.WorkPerm) + totalCnt++ + } + backupBetweenInserts := []int16{MaxLayerLen: int16(0)}[:len(lp.WorkPerm)] + // Apply permutation backwards. The only reason for this is: + // the "original, untouched" permutation ("insert nothing") is the last one, + // and that is the one we want to to win all other factors equal. So it goes first. + permIdx := curInsertSrc + for permIdx >= 0 { + copy(backupBetweenInserts, lp.WorkPerm) + // Shift-insert in-place + lp.insertPermutationByIdx(lp.WorkPerm, curInsertSrc, permIdx) + if curInsertSrc == insertSrcStart+insertSrcLen-1 { + f(totalCnt, lp.WorkPerm) + totalCnt++ + } else { + insertFuncIteratorRecursive(lp, insertSrcStart, insertSrcLen, curInsertSrc+1, totalCnt, cb) + } + copy(lp.WorkPerm, backupBetweenInserts) + permIdx-- + } +} + +func (lp *LayerPermutator) SwapAndInsertIterator(intervalStarts []int, intervalLengths []int, totalIntervals int, insertSrcStart int, insertSrcLen int, in []int16, f func(int, []int16)) { + lp.initSource(in) + lp.initIntervals(intervalStarts, intervalLengths, totalIntervals) + swapAndInsertFuncIterator(lp, 0, insertSrcStart, insertSrcLen, insertSrcStart, 0, f) +} + +func swapAndInsertFuncIterator(lp *LayerPermutator, intervalIdx int, insertSrcStart int, insertSrcLen int, curInsertSrc int, totalCnt int, f func(int, []int16)) { + realTotalCnt := 0 + cb := func(int, []int16) { + f(realTotalCnt, lp.WorkPerm) + realTotalCnt++ + } + swapFuncIteratorRecursive(lp, intervalIdx, totalCnt, func(int, []int16) { + insertFuncIteratorRecursive(lp, insertSrcStart, insertSrcLen, curInsertSrc, 0, cb) + }) +} diff --git a/pkg/capigraph/layer_permutator_test.go b/pkg/capigraph/layer_permutator_test.go new file mode 100644 index 00000000..a8ff87fd --- /dev/null +++ b/pkg/capigraph/layer_permutator_test.go @@ -0,0 +1,87 @@ +package capigraph + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var testPermStrategies6 = "[null,null,[[0],[1]],[[0,0],[0,1],[0,2],[1,0],[1,1],[1,2]],[[0,0,0],[0,0,1],[0,0,2],[0,0,3],[0,1,0],[0,1,1],[0,1,2],[0,1,3],[0,2,0],[0,2,1],[0,2,2],[0,2,3],[1,0,0],[1,0,1],[1,0,2],[1,0,3],[1,1,0],[1,1,1],[1,1,2],[1,1,3],[1,2,0],[1,2,1],[1,2,2],[1,2,3]],[[0,0,0,0],[0,0,0,1],[0,0,0,2],[0,0,0,3],[0,0,0,4],[0,0,1,0],[0,0,1,1],[0,0,1,2],[0,0,1,3],[0,0,1,4],[0,0,2,0],[0,0,2,1],[0,0,2,2],[0,0,2,3],[0,0,2,4],[0,0,3,0],[0,0,3,1],[0,0,3,2],[0,0,3,3],[0,0,3,4],[0,1,0,0],[0,1,0,1],[0,1,0,2],[0,1,0,3],[0,1,0,4],[0,1,1,0],[0,1,1,1],[0,1,1,2],[0,1,1,3],[0,1,1,4],[0,1,2,0],[0,1,2,1],[0,1,2,2],[0,1,2,3],[0,1,2,4],[0,1,3,0],[0,1,3,1],[0,1,3,2],[0,1,3,3],[0,1,3,4],[0,2,0,0],[0,2,0,1],[0,2,0,2],[0,2,0,3],[0,2,0,4],[0,2,1,0],[0,2,1,1],[0,2,1,2],[0,2,1,3],[0,2,1,4],[0,2,2,0],[0,2,2,1],[0,2,2,2],[0,2,2,3],[0,2,2,4],[0,2,3,0],[0,2,3,1],[0,2,3,2],[0,2,3,3],[0,2,3,4],[1,0,0,0],[1,0,0,1],[1,0,0,2],[1,0,0,3],[1,0,0,4],[1,0,1,0],[1,0,1,1],[1,0,1,2],[1,0,1,3],[1,0,1,4],[1,0,2,0],[1,0,2,1],[1,0,2,2],[1,0,2,3],[1,0,2,4],[1,0,3,0],[1,0,3,1],[1,0,3,2],[1,0,3,3],[1,0,3,4],[1,1,0,0],[1,1,0,1],[1,1,0,2],[1,1,0,3],[1,1,0,4],[1,1,1,0],[1,1,1,1],[1,1,1,2],[1,1,1,3],[1,1,1,4],[1,1,2,0],[1,1,2,1],[1,1,2,2],[1,1,2,3],[1,1,2,4],[1,1,3,0],[1,1,3,1],[1,1,3,2],[1,1,3,3],[1,1,3,4],[1,2,0,0],[1,2,0,1],[1,2,0,2],[1,2,0,3],[1,2,0,4],[1,2,1,0],[1,2,1,1],[1,2,1,2],[1,2,1,3],[1,2,1,4],[1,2,2,0],[1,2,2,1],[1,2,2,2],[1,2,2,3],[1,2,2,4],[1,2,3,0],[1,2,3,1],[1,2,3,2],[1,2,3,3],[1,2,3,4]],[[0,0,0,0,0],[0,0,0,0,1],[0,0,0,0,2],[0,0,0,0,3],[0,0,0,0,4],[0,0,0,0,5],[0,0,0,1,0],[0,0,0,1,1],[0,0,0,1,2],[0,0,0,1,3],[0,0,0,1,4],[0,0,0,1,5],[0,0,0,2,0],[0,0,0,2,1],[0,0,0,2,2],[0,0,0,2,3],[0,0,0,2,4],[0,0,0,2,5],[0,0,0,3,0],[0,0,0,3,1],[0,0,0,3,2],[0,0,0,3,3],[0,0,0,3,4],[0,0,0,3,5],[0,0,0,4,0],[0,0,0,4,1],[0,0,0,4,2],[0,0,0,4,3],[0,0,0,4,4],[0,0,0,4,5],[0,0,1,0,0],[0,0,1,0,1],[0,0,1,0,2],[0,0,1,0,3],[0,0,1,0,4],[0,0,1,0,5],[0,0,1,1,0],[0,0,1,1,1],[0,0,1,1,2],[0,0,1,1,3],[0,0,1,1,4],[0,0,1,1,5],[0,0,1,2,0],[0,0,1,2,1],[0,0,1,2,2],[0,0,1,2,3],[0,0,1,2,4],[0,0,1,2,5],[0,0,1,3,0],[0,0,1,3,1],[0,0,1,3,2],[0,0,1,3,3],[0,0,1,3,4],[0,0,1,3,5],[0,0,1,4,0],[0,0,1,4,1],[0,0,1,4,2],[0,0,1,4,3],[0,0,1,4,4],[0,0,1,4,5],[0,0,2,0,0],[0,0,2,0,1],[0,0,2,0,2],[0,0,2,0,3],[0,0,2,0,4],[0,0,2,0,5],[0,0,2,1,0],[0,0,2,1,1],[0,0,2,1,2],[0,0,2,1,3],[0,0,2,1,4],[0,0,2,1,5],[0,0,2,2,0],[0,0,2,2,1],[0,0,2,2,2],[0,0,2,2,3],[0,0,2,2,4],[0,0,2,2,5],[0,0,2,3,0],[0,0,2,3,1],[0,0,2,3,2],[0,0,2,3,3],[0,0,2,3,4],[0,0,2,3,5],[0,0,2,4,0],[0,0,2,4,1],[0,0,2,4,2],[0,0,2,4,3],[0,0,2,4,4],[0,0,2,4,5],[0,0,3,0,0],[0,0,3,0,1],[0,0,3,0,2],[0,0,3,0,3],[0,0,3,0,4],[0,0,3,0,5],[0,0,3,1,0],[0,0,3,1,1],[0,0,3,1,2],[0,0,3,1,3],[0,0,3,1,4],[0,0,3,1,5],[0,0,3,2,0],[0,0,3,2,1],[0,0,3,2,2],[0,0,3,2,3],[0,0,3,2,4],[0,0,3,2,5],[0,0,3,3,0],[0,0,3,3,1],[0,0,3,3,2],[0,0,3,3,3],[0,0,3,3,4],[0,0,3,3,5],[0,0,3,4,0],[0,0,3,4,1],[0,0,3,4,2],[0,0,3,4,3],[0,0,3,4,4],[0,0,3,4,5],[0,1,0,0,0],[0,1,0,0,1],[0,1,0,0,2],[0,1,0,0,3],[0,1,0,0,4],[0,1,0,0,5],[0,1,0,1,0],[0,1,0,1,1],[0,1,0,1,2],[0,1,0,1,3],[0,1,0,1,4],[0,1,0,1,5],[0,1,0,2,0],[0,1,0,2,1],[0,1,0,2,2],[0,1,0,2,3],[0,1,0,2,4],[0,1,0,2,5],[0,1,0,3,0],[0,1,0,3,1],[0,1,0,3,2],[0,1,0,3,3],[0,1,0,3,4],[0,1,0,3,5],[0,1,0,4,0],[0,1,0,4,1],[0,1,0,4,2],[0,1,0,4,3],[0,1,0,4,4],[0,1,0,4,5],[0,1,1,0,0],[0,1,1,0,1],[0,1,1,0,2],[0,1,1,0,3],[0,1,1,0,4],[0,1,1,0,5],[0,1,1,1,0],[0,1,1,1,1],[0,1,1,1,2],[0,1,1,1,3],[0,1,1,1,4],[0,1,1,1,5],[0,1,1,2,0],[0,1,1,2,1],[0,1,1,2,2],[0,1,1,2,3],[0,1,1,2,4],[0,1,1,2,5],[0,1,1,3,0],[0,1,1,3,1],[0,1,1,3,2],[0,1,1,3,3],[0,1,1,3,4],[0,1,1,3,5],[0,1,1,4,0],[0,1,1,4,1],[0,1,1,4,2],[0,1,1,4,3],[0,1,1,4,4],[0,1,1,4,5],[0,1,2,0,0],[0,1,2,0,1],[0,1,2,0,2],[0,1,2,0,3],[0,1,2,0,4],[0,1,2,0,5],[0,1,2,1,0],[0,1,2,1,1],[0,1,2,1,2],[0,1,2,1,3],[0,1,2,1,4],[0,1,2,1,5],[0,1,2,2,0],[0,1,2,2,1],[0,1,2,2,2],[0,1,2,2,3],[0,1,2,2,4],[0,1,2,2,5],[0,1,2,3,0],[0,1,2,3,1],[0,1,2,3,2],[0,1,2,3,3],[0,1,2,3,4],[0,1,2,3,5],[0,1,2,4,0],[0,1,2,4,1],[0,1,2,4,2],[0,1,2,4,3],[0,1,2,4,4],[0,1,2,4,5],[0,1,3,0,0],[0,1,3,0,1],[0,1,3,0,2],[0,1,3,0,3],[0,1,3,0,4],[0,1,3,0,5],[0,1,3,1,0],[0,1,3,1,1],[0,1,3,1,2],[0,1,3,1,3],[0,1,3,1,4],[0,1,3,1,5],[0,1,3,2,0],[0,1,3,2,1],[0,1,3,2,2],[0,1,3,2,3],[0,1,3,2,4],[0,1,3,2,5],[0,1,3,3,0],[0,1,3,3,1],[0,1,3,3,2],[0,1,3,3,3],[0,1,3,3,4],[0,1,3,3,5],[0,1,3,4,0],[0,1,3,4,1],[0,1,3,4,2],[0,1,3,4,3],[0,1,3,4,4],[0,1,3,4,5],[0,2,0,0,0],[0,2,0,0,1],[0,2,0,0,2],[0,2,0,0,3],[0,2,0,0,4],[0,2,0,0,5],[0,2,0,1,0],[0,2,0,1,1],[0,2,0,1,2],[0,2,0,1,3],[0,2,0,1,4],[0,2,0,1,5],[0,2,0,2,0],[0,2,0,2,1],[0,2,0,2,2],[0,2,0,2,3],[0,2,0,2,4],[0,2,0,2,5],[0,2,0,3,0],[0,2,0,3,1],[0,2,0,3,2],[0,2,0,3,3],[0,2,0,3,4],[0,2,0,3,5],[0,2,0,4,0],[0,2,0,4,1],[0,2,0,4,2],[0,2,0,4,3],[0,2,0,4,4],[0,2,0,4,5],[0,2,1,0,0],[0,2,1,0,1],[0,2,1,0,2],[0,2,1,0,3],[0,2,1,0,4],[0,2,1,0,5],[0,2,1,1,0],[0,2,1,1,1],[0,2,1,1,2],[0,2,1,1,3],[0,2,1,1,4],[0,2,1,1,5],[0,2,1,2,0],[0,2,1,2,1],[0,2,1,2,2],[0,2,1,2,3],[0,2,1,2,4],[0,2,1,2,5],[0,2,1,3,0],[0,2,1,3,1],[0,2,1,3,2],[0,2,1,3,3],[0,2,1,3,4],[0,2,1,3,5],[0,2,1,4,0],[0,2,1,4,1],[0,2,1,4,2],[0,2,1,4,3],[0,2,1,4,4],[0,2,1,4,5],[0,2,2,0,0],[0,2,2,0,1],[0,2,2,0,2],[0,2,2,0,3],[0,2,2,0,4],[0,2,2,0,5],[0,2,2,1,0],[0,2,2,1,1],[0,2,2,1,2],[0,2,2,1,3],[0,2,2,1,4],[0,2,2,1,5],[0,2,2,2,0],[0,2,2,2,1],[0,2,2,2,2],[0,2,2,2,3],[0,2,2,2,4],[0,2,2,2,5],[0,2,2,3,0],[0,2,2,3,1],[0,2,2,3,2],[0,2,2,3,3],[0,2,2,3,4],[0,2,2,3,5],[0,2,2,4,0],[0,2,2,4,1],[0,2,2,4,2],[0,2,2,4,3],[0,2,2,4,4],[0,2,2,4,5],[0,2,3,0,0],[0,2,3,0,1],[0,2,3,0,2],[0,2,3,0,3],[0,2,3,0,4],[0,2,3,0,5],[0,2,3,1,0],[0,2,3,1,1],[0,2,3,1,2],[0,2,3,1,3],[0,2,3,1,4],[0,2,3,1,5],[0,2,3,2,0],[0,2,3,2,1],[0,2,3,2,2],[0,2,3,2,3],[0,2,3,2,4],[0,2,3,2,5],[0,2,3,3,0],[0,2,3,3,1],[0,2,3,3,2],[0,2,3,3,3],[0,2,3,3,4],[0,2,3,3,5],[0,2,3,4,0],[0,2,3,4,1],[0,2,3,4,2],[0,2,3,4,3],[0,2,3,4,4],[0,2,3,4,5],[1,0,0,0,0],[1,0,0,0,1],[1,0,0,0,2],[1,0,0,0,3],[1,0,0,0,4],[1,0,0,0,5],[1,0,0,1,0],[1,0,0,1,1],[1,0,0,1,2],[1,0,0,1,3],[1,0,0,1,4],[1,0,0,1,5],[1,0,0,2,0],[1,0,0,2,1],[1,0,0,2,2],[1,0,0,2,3],[1,0,0,2,4],[1,0,0,2,5],[1,0,0,3,0],[1,0,0,3,1],[1,0,0,3,2],[1,0,0,3,3],[1,0,0,3,4],[1,0,0,3,5],[1,0,0,4,0],[1,0,0,4,1],[1,0,0,4,2],[1,0,0,4,3],[1,0,0,4,4],[1,0,0,4,5],[1,0,1,0,0],[1,0,1,0,1],[1,0,1,0,2],[1,0,1,0,3],[1,0,1,0,4],[1,0,1,0,5],[1,0,1,1,0],[1,0,1,1,1],[1,0,1,1,2],[1,0,1,1,3],[1,0,1,1,4],[1,0,1,1,5],[1,0,1,2,0],[1,0,1,2,1],[1,0,1,2,2],[1,0,1,2,3],[1,0,1,2,4],[1,0,1,2,5],[1,0,1,3,0],[1,0,1,3,1],[1,0,1,3,2],[1,0,1,3,3],[1,0,1,3,4],[1,0,1,3,5],[1,0,1,4,0],[1,0,1,4,1],[1,0,1,4,2],[1,0,1,4,3],[1,0,1,4,4],[1,0,1,4,5],[1,0,2,0,0],[1,0,2,0,1],[1,0,2,0,2],[1,0,2,0,3],[1,0,2,0,4],[1,0,2,0,5],[1,0,2,1,0],[1,0,2,1,1],[1,0,2,1,2],[1,0,2,1,3],[1,0,2,1,4],[1,0,2,1,5],[1,0,2,2,0],[1,0,2,2,1],[1,0,2,2,2],[1,0,2,2,3],[1,0,2,2,4],[1,0,2,2,5],[1,0,2,3,0],[1,0,2,3,1],[1,0,2,3,2],[1,0,2,3,3],[1,0,2,3,4],[1,0,2,3,5],[1,0,2,4,0],[1,0,2,4,1],[1,0,2,4,2],[1,0,2,4,3],[1,0,2,4,4],[1,0,2,4,5],[1,0,3,0,0],[1,0,3,0,1],[1,0,3,0,2],[1,0,3,0,3],[1,0,3,0,4],[1,0,3,0,5],[1,0,3,1,0],[1,0,3,1,1],[1,0,3,1,2],[1,0,3,1,3],[1,0,3,1,4],[1,0,3,1,5],[1,0,3,2,0],[1,0,3,2,1],[1,0,3,2,2],[1,0,3,2,3],[1,0,3,2,4],[1,0,3,2,5],[1,0,3,3,0],[1,0,3,3,1],[1,0,3,3,2],[1,0,3,3,3],[1,0,3,3,4],[1,0,3,3,5],[1,0,3,4,0],[1,0,3,4,1],[1,0,3,4,2],[1,0,3,4,3],[1,0,3,4,4],[1,0,3,4,5],[1,1,0,0,0],[1,1,0,0,1],[1,1,0,0,2],[1,1,0,0,3],[1,1,0,0,4],[1,1,0,0,5],[1,1,0,1,0],[1,1,0,1,1],[1,1,0,1,2],[1,1,0,1,3],[1,1,0,1,4],[1,1,0,1,5],[1,1,0,2,0],[1,1,0,2,1],[1,1,0,2,2],[1,1,0,2,3],[1,1,0,2,4],[1,1,0,2,5],[1,1,0,3,0],[1,1,0,3,1],[1,1,0,3,2],[1,1,0,3,3],[1,1,0,3,4],[1,1,0,3,5],[1,1,0,4,0],[1,1,0,4,1],[1,1,0,4,2],[1,1,0,4,3],[1,1,0,4,4],[1,1,0,4,5],[1,1,1,0,0],[1,1,1,0,1],[1,1,1,0,2],[1,1,1,0,3],[1,1,1,0,4],[1,1,1,0,5],[1,1,1,1,0],[1,1,1,1,1],[1,1,1,1,2],[1,1,1,1,3],[1,1,1,1,4],[1,1,1,1,5],[1,1,1,2,0],[1,1,1,2,1],[1,1,1,2,2],[1,1,1,2,3],[1,1,1,2,4],[1,1,1,2,5],[1,1,1,3,0],[1,1,1,3,1],[1,1,1,3,2],[1,1,1,3,3],[1,1,1,3,4],[1,1,1,3,5],[1,1,1,4,0],[1,1,1,4,1],[1,1,1,4,2],[1,1,1,4,3],[1,1,1,4,4],[1,1,1,4,5],[1,1,2,0,0],[1,1,2,0,1],[1,1,2,0,2],[1,1,2,0,3],[1,1,2,0,4],[1,1,2,0,5],[1,1,2,1,0],[1,1,2,1,1],[1,1,2,1,2],[1,1,2,1,3],[1,1,2,1,4],[1,1,2,1,5],[1,1,2,2,0],[1,1,2,2,1],[1,1,2,2,2],[1,1,2,2,3],[1,1,2,2,4],[1,1,2,2,5],[1,1,2,3,0],[1,1,2,3,1],[1,1,2,3,2],[1,1,2,3,3],[1,1,2,3,4],[1,1,2,3,5],[1,1,2,4,0],[1,1,2,4,1],[1,1,2,4,2],[1,1,2,4,3],[1,1,2,4,4],[1,1,2,4,5],[1,1,3,0,0],[1,1,3,0,1],[1,1,3,0,2],[1,1,3,0,3],[1,1,3,0,4],[1,1,3,0,5],[1,1,3,1,0],[1,1,3,1,1],[1,1,3,1,2],[1,1,3,1,3],[1,1,3,1,4],[1,1,3,1,5],[1,1,3,2,0],[1,1,3,2,1],[1,1,3,2,2],[1,1,3,2,3],[1,1,3,2,4],[1,1,3,2,5],[1,1,3,3,0],[1,1,3,3,1],[1,1,3,3,2],[1,1,3,3,3],[1,1,3,3,4],[1,1,3,3,5],[1,1,3,4,0],[1,1,3,4,1],[1,1,3,4,2],[1,1,3,4,3],[1,1,3,4,4],[1,1,3,4,5],[1,2,0,0,0],[1,2,0,0,1],[1,2,0,0,2],[1,2,0,0,3],[1,2,0,0,4],[1,2,0,0,5],[1,2,0,1,0],[1,2,0,1,1],[1,2,0,1,2],[1,2,0,1,3],[1,2,0,1,4],[1,2,0,1,5],[1,2,0,2,0],[1,2,0,2,1],[1,2,0,2,2],[1,2,0,2,3],[1,2,0,2,4],[1,2,0,2,5],[1,2,0,3,0],[1,2,0,3,1],[1,2,0,3,2],[1,2,0,3,3],[1,2,0,3,4],[1,2,0,3,5],[1,2,0,4,0],[1,2,0,4,1],[1,2,0,4,2],[1,2,0,4,3],[1,2,0,4,4],[1,2,0,4,5],[1,2,1,0,0],[1,2,1,0,1],[1,2,1,0,2],[1,2,1,0,3],[1,2,1,0,4],[1,2,1,0,5],[1,2,1,1,0],[1,2,1,1,1],[1,2,1,1,2],[1,2,1,1,3],[1,2,1,1,4],[1,2,1,1,5],[1,2,1,2,0],[1,2,1,2,1],[1,2,1,2,2],[1,2,1,2,3],[1,2,1,2,4],[1,2,1,2,5],[1,2,1,3,0],[1,2,1,3,1],[1,2,1,3,2],[1,2,1,3,3],[1,2,1,3,4],[1,2,1,3,5],[1,2,1,4,0],[1,2,1,4,1],[1,2,1,4,2],[1,2,1,4,3],[1,2,1,4,4],[1,2,1,4,5],[1,2,2,0,0],[1,2,2,0,1],[1,2,2,0,2],[1,2,2,0,3],[1,2,2,0,4],[1,2,2,0,5],[1,2,2,1,0],[1,2,2,1,1],[1,2,2,1,2],[1,2,2,1,3],[1,2,2,1,4],[1,2,2,1,5],[1,2,2,2,0],[1,2,2,2,1],[1,2,2,2,2],[1,2,2,2,3],[1,2,2,2,4],[1,2,2,2,5],[1,2,2,3,0],[1,2,2,3,1],[1,2,2,3,2],[1,2,2,3,3],[1,2,2,3,4],[1,2,2,3,5],[1,2,2,4,0],[1,2,2,4,1],[1,2,2,4,2],[1,2,2,4,3],[1,2,2,4,4],[1,2,2,4,5],[1,2,3,0,0],[1,2,3,0,1],[1,2,3,0,2],[1,2,3,0,3],[1,2,3,0,4],[1,2,3,0,5],[1,2,3,1,0],[1,2,3,1,1],[1,2,3,1,2],[1,2,3,1,3],[1,2,3,1,4],[1,2,3,1,5],[1,2,3,2,0],[1,2,3,2,1],[1,2,3,2,2],[1,2,3,2,3],[1,2,3,2,4],[1,2,3,2,5],[1,2,3,3,0],[1,2,3,3,1],[1,2,3,3,2],[1,2,3,3,3],[1,2,3,3,4],[1,2,3,3,5],[1,2,3,4,0],[1,2,3,4,1],[1,2,3,4,2],[1,2,3,4,3],[1,2,3,4,4],[1,2,3,4,5]]]" +var testFact6 = "[1,1,2,6,24,120,720]" + +func TestInit(t *testing.T) { + lp := NewLayerPermutator() + b, _ := json.Marshal(lp.P) + assert.Equal(t, testPermStrategies6, string(b)) + b, _ = json.Marshal(lp.Fact) + assert.Equal(t, testFact6, string(b)) +} + +func TestSwapFunc(t *testing.T) { + lp := NewLayerPermutator() + intervalStarts := []int{1, 6} + intervalLengths := []int{4, 2} + in := []int16{-1, 10, 20, 30, 40, -2, 100, 200, -3} + sb := strings.Builder{} + lp.SwapIterator(intervalStarts, intervalLengths, len(intervalLengths), in, func(totalCnt int, perm []int16) { + if sb.Len() > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("%d:%v", totalCnt, perm)) + }) + assert.Equal(t, "0:[-1 10 20 30 40 -2 100 200 -3], 1:[-1 10 20 30 40 -2 200 100 -3], 2:[-1 10 20 40 30 -2 100 200 -3], 3:[-1 10 20 40 30 -2 200 100 -3], 4:[-1 10 40 30 20 -2 100 200 -3], 5:[-1 10 40 30 20 -2 200 100 -3], 6:[-1 40 20 30 10 -2 100 200 -3], 7:[-1 40 20 30 10 -2 200 100 -3], 8:[-1 10 30 20 40 -2 100 200 -3], 9:[-1 10 30 20 40 -2 200 100 -3], 10:[-1 10 30 40 20 -2 100 200 -3], 11:[-1 10 30 40 20 -2 200 100 -3], 12:[-1 10 40 20 30 -2 100 200 -3], 13:[-1 10 40 20 30 -2 200 100 -3], 14:[-1 40 30 20 10 -2 100 200 -3], 15:[-1 40 30 20 10 -2 200 100 -3], 16:[-1 30 20 10 40 -2 100 200 -3], 17:[-1 30 20 10 40 -2 200 100 -3], 18:[-1 30 20 40 10 -2 100 200 -3], 19:[-1 30 20 40 10 -2 200 100 -3], 20:[-1 30 40 10 20 -2 100 200 -3], 21:[-1 30 40 10 20 -2 200 100 -3], 22:[-1 40 20 10 30 -2 100 200 -3], 23:[-1 40 20 10 30 -2 200 100 -3], 24:[-1 20 10 30 40 -2 100 200 -3], 25:[-1 20 10 30 40 -2 200 100 -3], 26:[-1 20 10 40 30 -2 100 200 -3], 27:[-1 20 10 40 30 -2 200 100 -3], 28:[-1 20 40 30 10 -2 100 200 -3], 29:[-1 20 40 30 10 -2 200 100 -3], 30:[-1 40 10 30 20 -2 100 200 -3], 31:[-1 40 10 30 20 -2 200 100 -3], 32:[-1 20 30 10 40 -2 100 200 -3], 33:[-1 20 30 10 40 -2 200 100 -3], 34:[-1 20 30 40 10 -2 100 200 -3], 35:[-1 20 30 40 10 -2 200 100 -3], 36:[-1 20 40 10 30 -2 100 200 -3], 37:[-1 20 40 10 30 -2 200 100 -3], 38:[-1 40 30 10 20 -2 100 200 -3], 39:[-1 40 30 10 20 -2 200 100 -3], 40:[-1 30 10 20 40 -2 100 200 -3], 41:[-1 30 10 20 40 -2 200 100 -3], 42:[-1 30 10 40 20 -2 100 200 -3], 43:[-1 30 10 40 20 -2 200 100 -3], 44:[-1 30 40 20 10 -2 100 200 -3], 45:[-1 30 40 20 10 -2 200 100 -3], 46:[-1 40 10 20 30 -2 100 200 -3], 47:[-1 40 10 20 30 -2 200 100 -3]", sb.String()) +} + +func TestInsert(t *testing.T) { + lp := NewLayerPermutator() + + in := []int16{-1, -2, -3, -4, 100} + lp.insertPermutationByIdx(in, 4, 1) + assert.Equal(t, "[-1 100 -2 -3 -4]", fmt.Sprintf("%v", in)) + + // Insert 100 and 200 starting at 0, 6x7=42 variants + in = []int16{1, 2, 3, 4, 5, 100, 200} + sb := strings.Builder{} + lp.InsertIterator(5, 2, in, func(totalCnt int, perm []int16) { + if sb.Len() > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("%d:%v", totalCnt, perm)) + }) + assert.Equal(t, "0:[1 2 3 4 5 100 200], 1:[1 2 3 4 5 200 100], 2:[1 2 3 4 200 5 100], 3:[1 2 3 200 4 5 100], 4:[1 2 200 3 4 5 100], 5:[1 200 2 3 4 5 100], 6:[200 1 2 3 4 5 100], 7:[1 2 3 4 100 5 200], 8:[1 2 3 4 100 200 5], 9:[1 2 3 4 200 100 5], 10:[1 2 3 200 4 100 5], 11:[1 2 200 3 4 100 5], 12:[1 200 2 3 4 100 5], 13:[200 1 2 3 4 100 5], 14:[1 2 3 100 4 5 200], 15:[1 2 3 100 4 200 5], 16:[1 2 3 100 200 4 5], 17:[1 2 3 200 100 4 5], 18:[1 2 200 3 100 4 5], 19:[1 200 2 3 100 4 5], 20:[200 1 2 3 100 4 5], 21:[1 2 100 3 4 5 200], 22:[1 2 100 3 4 200 5], 23:[1 2 100 3 200 4 5], 24:[1 2 100 200 3 4 5], 25:[1 2 200 100 3 4 5], 26:[1 200 2 100 3 4 5], 27:[200 1 2 100 3 4 5], 28:[1 100 2 3 4 5 200], 29:[1 100 2 3 4 200 5], 30:[1 100 2 3 200 4 5], 31:[1 100 2 200 3 4 5], 32:[1 100 200 2 3 4 5], 33:[1 200 100 2 3 4 5], 34:[200 1 100 2 3 4 5], 35:[100 1 2 3 4 5 200], 36:[100 1 2 3 4 200 5], 37:[100 1 2 3 200 4 5], 38:[100 1 2 200 3 4 5], 39:[100 1 200 2 3 4 5], 40:[100 200 1 2 3 4 5], 41:[200 100 1 2 3 4 5]", sb.String()) +} + +/* + func TestSwap(t *testing.T) { + lp := NewLayerPermutator() + + // All permutations of 10/20/30/40 and 100/200 intervals + permIntervalStarts := [20]int{1, 6} + permIntervalLengths := [20]int{4, 2} + a := []int16{-1, 10, 20, 30, 40, -2, 100, 200, -3} + sb := strings.Builder{} + for i, perm := range lp.SwapPermIterator(&permIntervalStarts, &permIntervalLengths, 2, a) { + sb.WriteString(fmt.Sprintf("%d %v, ", i, perm)) + } + assert.Equal(t, "0 [-1 40 10 20 30 -2 200 100 -3], 1 [-1 40 10 20 30 -2 100 200 -3], 2 [-1 30 40 20 10 -2 200 100 -3], 3 [-1 30 40 20 10 -2 100 200 -3], 4 [-1 30 10 40 20 -2 200 100 -3], 5 [-1 30 10 40 20 -2 100 200 -3], 6 [-1 30 10 20 40 -2 200 100 -3], 7 [-1 30 10 20 40 -2 100 200 -3], 8 [-1 40 30 10 20 -2 200 100 -3], 9 [-1 40 30 10 20 -2 100 200 -3], 10 [-1 20 40 10 30 -2 200 100 -3], 11 [-1 20 40 10 30 -2 100 200 -3], 12 [-1 20 30 40 10 -2 200 100 -3], 13 [-1 20 30 40 10 -2 100 200 -3], 14 [-1 20 30 10 40 -2 200 100 -3], 15 [-1 20 30 10 40 -2 100 200 -3], 16 [-1 40 10 30 20 -2 200 100 -3], 17 [-1 40 10 30 20 -2 100 200 -3], 18 [-1 20 40 30 10 -2 200 100 -3], 19 [-1 20 40 30 10 -2 100 200 -3], 20 [-1 20 10 40 30 -2 200 100 -3], 21 [-1 20 10 40 30 -2 100 200 -3], 22 [-1 20 10 30 40 -2 200 100 -3], 23 [-1 20 10 30 40 -2 100 200 -3], 24 [-1 40 20 10 30 -2 200 100 -3], 25 [-1 40 20 10 30 -2 100 200 -3], 26 [-1 30 40 10 20 -2 200 100 -3], 27 [-1 30 40 10 20 -2 100 200 -3], 28 [-1 30 20 40 10 -2 200 100 -3], 29 [-1 30 20 40 10 -2 100 200 -3], 30 [-1 30 20 10 40 -2 200 100 -3], 31 [-1 30 20 10 40 -2 100 200 -3], 32 [-1 40 30 20 10 -2 200 100 -3], 33 [-1 40 30 20 10 -2 100 200 -3], 34 [-1 10 40 20 30 -2 200 100 -3], 35 [-1 10 40 20 30 -2 100 200 -3], 36 [-1 10 30 40 20 -2 200 100 -3], 37 [-1 10 30 40 20 -2 100 200 -3], 38 [-1 10 30 20 40 -2 200 100 -3], 39 [-1 10 30 20 40 -2 100 200 -3], 40 [-1 40 20 30 10 -2 200 100 -3], 41 [-1 40 20 30 10 -2 100 200 -3], 42 [-1 10 40 30 20 -2 200 100 -3], 43 [-1 10 40 30 20 -2 100 200 -3], 44 [-1 10 20 40 30 -2 200 100 -3], 45 [-1 10 20 40 30 -2 100 200 -3], 46 [-1 10 20 30 40 -2 200 100 -3], 47 [-1 10 20 30 40 -2 100 200 -3], ", sb.String()) + } +*/ +func TestSwapAndInsert(t *testing.T) { + lp := NewLayerPermutator() + + // Swap permutations of 10/20 and 100/200, and insert 1000 and 2000 before 1000, total 2x2x8*9=288 final permutations + in := []int16{-1, 10, 20, -2, 100, 200, -3, 1000, 2000} + intervalStarts := []int{1, 4} + intervalLengths := []int{2, 2} + sb := strings.Builder{} + lp.SwapAndInsertIterator(intervalStarts, intervalLengths, len(intervalStarts), 7, 2, in, func(totalCnt int, perm []int16) { + if sb.Len() > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("%d:%v", totalCnt, perm)) + }) + assert.Equal(t, "0:[-1 10 20 -2 100 200 -3 1000 2000], 1:[-1 10 20 -2 100 200 -3 2000 1000], 2:[-1 10 20 -2 100 200 2000 -3 1000], 3:[-1 10 20 -2 100 2000 200 -3 1000], 4:[-1 10 20 -2 2000 100 200 -3 1000], 5:[-1 10 20 2000 -2 100 200 -3 1000], 6:[-1 10 2000 20 -2 100 200 -3 1000], 7:[-1 2000 10 20 -2 100 200 -3 1000], 8:[2000 -1 10 20 -2 100 200 -3 1000], 9:[-1 10 20 -2 100 200 1000 -3 2000], 10:[-1 10 20 -2 100 200 1000 2000 -3], 11:[-1 10 20 -2 100 200 2000 1000 -3], 12:[-1 10 20 -2 100 2000 200 1000 -3], 13:[-1 10 20 -2 2000 100 200 1000 -3], 14:[-1 10 20 2000 -2 100 200 1000 -3], 15:[-1 10 2000 20 -2 100 200 1000 -3], 16:[-1 2000 10 20 -2 100 200 1000 -3], 17:[2000 -1 10 20 -2 100 200 1000 -3], 18:[-1 10 20 -2 100 1000 200 -3 2000], 19:[-1 10 20 -2 100 1000 200 2000 -3], 20:[-1 10 20 -2 100 1000 2000 200 -3], 21:[-1 10 20 -2 100 2000 1000 200 -3], 22:[-1 10 20 -2 2000 100 1000 200 -3], 23:[-1 10 20 2000 -2 100 1000 200 -3], 24:[-1 10 2000 20 -2 100 1000 200 -3], 25:[-1 2000 10 20 -2 100 1000 200 -3], 26:[2000 -1 10 20 -2 100 1000 200 -3], 27:[-1 10 20 -2 1000 100 200 -3 2000], 28:[-1 10 20 -2 1000 100 200 2000 -3], 29:[-1 10 20 -2 1000 100 2000 200 -3], 30:[-1 10 20 -2 1000 2000 100 200 -3], 31:[-1 10 20 -2 2000 1000 100 200 -3], 32:[-1 10 20 2000 -2 1000 100 200 -3], 33:[-1 10 2000 20 -2 1000 100 200 -3], 34:[-1 2000 10 20 -2 1000 100 200 -3], 35:[2000 -1 10 20 -2 1000 100 200 -3], 36:[-1 10 20 1000 -2 100 200 -3 2000], 37:[-1 10 20 1000 -2 100 200 2000 -3], 38:[-1 10 20 1000 -2 100 2000 200 -3], 39:[-1 10 20 1000 -2 2000 100 200 -3], 40:[-1 10 20 1000 2000 -2 100 200 -3], 41:[-1 10 20 2000 1000 -2 100 200 -3], 42:[-1 10 2000 20 1000 -2 100 200 -3], 43:[-1 2000 10 20 1000 -2 100 200 -3], 44:[2000 -1 10 20 1000 -2 100 200 -3], 45:[-1 10 1000 20 -2 100 200 -3 2000], 46:[-1 10 1000 20 -2 100 200 2000 -3], 47:[-1 10 1000 20 -2 100 2000 200 -3], 48:[-1 10 1000 20 -2 2000 100 200 -3], 49:[-1 10 1000 20 2000 -2 100 200 -3], 50:[-1 10 1000 2000 20 -2 100 200 -3], 51:[-1 10 2000 1000 20 -2 100 200 -3], 52:[-1 2000 10 1000 20 -2 100 200 -3], 53:[2000 -1 10 1000 20 -2 100 200 -3], 54:[-1 1000 10 20 -2 100 200 -3 2000], 55:[-1 1000 10 20 -2 100 200 2000 -3], 56:[-1 1000 10 20 -2 100 2000 200 -3], 57:[-1 1000 10 20 -2 2000 100 200 -3], 58:[-1 1000 10 20 2000 -2 100 200 -3], 59:[-1 1000 10 2000 20 -2 100 200 -3], 60:[-1 1000 2000 10 20 -2 100 200 -3], 61:[-1 2000 1000 10 20 -2 100 200 -3], 62:[2000 -1 1000 10 20 -2 100 200 -3], 63:[1000 -1 10 20 -2 100 200 -3 2000], 64:[1000 -1 10 20 -2 100 200 2000 -3], 65:[1000 -1 10 20 -2 100 2000 200 -3], 66:[1000 -1 10 20 -2 2000 100 200 -3], 67:[1000 -1 10 20 2000 -2 100 200 -3], 68:[1000 -1 10 2000 20 -2 100 200 -3], 69:[1000 -1 2000 10 20 -2 100 200 -3], 70:[1000 2000 -1 10 20 -2 100 200 -3], 71:[2000 1000 -1 10 20 -2 100 200 -3], 72:[-1 10 20 -2 200 100 -3 1000 2000], 73:[-1 10 20 -2 200 100 -3 2000 1000], 74:[-1 10 20 -2 200 100 2000 -3 1000], 75:[-1 10 20 -2 200 2000 100 -3 1000], 76:[-1 10 20 -2 2000 200 100 -3 1000], 77:[-1 10 20 2000 -2 200 100 -3 1000], 78:[-1 10 2000 20 -2 200 100 -3 1000], 79:[-1 2000 10 20 -2 200 100 -3 1000], 80:[2000 -1 10 20 -2 200 100 -3 1000], 81:[-1 10 20 -2 200 100 1000 -3 2000], 82:[-1 10 20 -2 200 100 1000 2000 -3], 83:[-1 10 20 -2 200 100 2000 1000 -3], 84:[-1 10 20 -2 200 2000 100 1000 -3], 85:[-1 10 20 -2 2000 200 100 1000 -3], 86:[-1 10 20 2000 -2 200 100 1000 -3], 87:[-1 10 2000 20 -2 200 100 1000 -3], 88:[-1 2000 10 20 -2 200 100 1000 -3], 89:[2000 -1 10 20 -2 200 100 1000 -3], 90:[-1 10 20 -2 200 1000 100 -3 2000], 91:[-1 10 20 -2 200 1000 100 2000 -3], 92:[-1 10 20 -2 200 1000 2000 100 -3], 93:[-1 10 20 -2 200 2000 1000 100 -3], 94:[-1 10 20 -2 2000 200 1000 100 -3], 95:[-1 10 20 2000 -2 200 1000 100 -3], 96:[-1 10 2000 20 -2 200 1000 100 -3], 97:[-1 2000 10 20 -2 200 1000 100 -3], 98:[2000 -1 10 20 -2 200 1000 100 -3], 99:[-1 10 20 -2 1000 200 100 -3 2000], 100:[-1 10 20 -2 1000 200 100 2000 -3], 101:[-1 10 20 -2 1000 200 2000 100 -3], 102:[-1 10 20 -2 1000 2000 200 100 -3], 103:[-1 10 20 -2 2000 1000 200 100 -3], 104:[-1 10 20 2000 -2 1000 200 100 -3], 105:[-1 10 2000 20 -2 1000 200 100 -3], 106:[-1 2000 10 20 -2 1000 200 100 -3], 107:[2000 -1 10 20 -2 1000 200 100 -3], 108:[-1 10 20 1000 -2 200 100 -3 2000], 109:[-1 10 20 1000 -2 200 100 2000 -3], 110:[-1 10 20 1000 -2 200 2000 100 -3], 111:[-1 10 20 1000 -2 2000 200 100 -3], 112:[-1 10 20 1000 2000 -2 200 100 -3], 113:[-1 10 20 2000 1000 -2 200 100 -3], 114:[-1 10 2000 20 1000 -2 200 100 -3], 115:[-1 2000 10 20 1000 -2 200 100 -3], 116:[2000 -1 10 20 1000 -2 200 100 -3], 117:[-1 10 1000 20 -2 200 100 -3 2000], 118:[-1 10 1000 20 -2 200 100 2000 -3], 119:[-1 10 1000 20 -2 200 2000 100 -3], 120:[-1 10 1000 20 -2 2000 200 100 -3], 121:[-1 10 1000 20 2000 -2 200 100 -3], 122:[-1 10 1000 2000 20 -2 200 100 -3], 123:[-1 10 2000 1000 20 -2 200 100 -3], 124:[-1 2000 10 1000 20 -2 200 100 -3], 125:[2000 -1 10 1000 20 -2 200 100 -3], 126:[-1 1000 10 20 -2 200 100 -3 2000], 127:[-1 1000 10 20 -2 200 100 2000 -3], 128:[-1 1000 10 20 -2 200 2000 100 -3], 129:[-1 1000 10 20 -2 2000 200 100 -3], 130:[-1 1000 10 20 2000 -2 200 100 -3], 131:[-1 1000 10 2000 20 -2 200 100 -3], 132:[-1 1000 2000 10 20 -2 200 100 -3], 133:[-1 2000 1000 10 20 -2 200 100 -3], 134:[2000 -1 1000 10 20 -2 200 100 -3], 135:[1000 -1 10 20 -2 200 100 -3 2000], 136:[1000 -1 10 20 -2 200 100 2000 -3], 137:[1000 -1 10 20 -2 200 2000 100 -3], 138:[1000 -1 10 20 -2 2000 200 100 -3], 139:[1000 -1 10 20 2000 -2 200 100 -3], 140:[1000 -1 10 2000 20 -2 200 100 -3], 141:[1000 -1 2000 10 20 -2 200 100 -3], 142:[1000 2000 -1 10 20 -2 200 100 -3], 143:[2000 1000 -1 10 20 -2 200 100 -3], 144:[-1 20 10 -2 100 200 -3 1000 2000], 145:[-1 20 10 -2 100 200 -3 2000 1000], 146:[-1 20 10 -2 100 200 2000 -3 1000], 147:[-1 20 10 -2 100 2000 200 -3 1000], 148:[-1 20 10 -2 2000 100 200 -3 1000], 149:[-1 20 10 2000 -2 100 200 -3 1000], 150:[-1 20 2000 10 -2 100 200 -3 1000], 151:[-1 2000 20 10 -2 100 200 -3 1000], 152:[2000 -1 20 10 -2 100 200 -3 1000], 153:[-1 20 10 -2 100 200 1000 -3 2000], 154:[-1 20 10 -2 100 200 1000 2000 -3], 155:[-1 20 10 -2 100 200 2000 1000 -3], 156:[-1 20 10 -2 100 2000 200 1000 -3], 157:[-1 20 10 -2 2000 100 200 1000 -3], 158:[-1 20 10 2000 -2 100 200 1000 -3], 159:[-1 20 2000 10 -2 100 200 1000 -3], 160:[-1 2000 20 10 -2 100 200 1000 -3], 161:[2000 -1 20 10 -2 100 200 1000 -3], 162:[-1 20 10 -2 100 1000 200 -3 2000], 163:[-1 20 10 -2 100 1000 200 2000 -3], 164:[-1 20 10 -2 100 1000 2000 200 -3], 165:[-1 20 10 -2 100 2000 1000 200 -3], 166:[-1 20 10 -2 2000 100 1000 200 -3], 167:[-1 20 10 2000 -2 100 1000 200 -3], 168:[-1 20 2000 10 -2 100 1000 200 -3], 169:[-1 2000 20 10 -2 100 1000 200 -3], 170:[2000 -1 20 10 -2 100 1000 200 -3], 171:[-1 20 10 -2 1000 100 200 -3 2000], 172:[-1 20 10 -2 1000 100 200 2000 -3], 173:[-1 20 10 -2 1000 100 2000 200 -3], 174:[-1 20 10 -2 1000 2000 100 200 -3], 175:[-1 20 10 -2 2000 1000 100 200 -3], 176:[-1 20 10 2000 -2 1000 100 200 -3], 177:[-1 20 2000 10 -2 1000 100 200 -3], 178:[-1 2000 20 10 -2 1000 100 200 -3], 179:[2000 -1 20 10 -2 1000 100 200 -3], 180:[-1 20 10 1000 -2 100 200 -3 2000], 181:[-1 20 10 1000 -2 100 200 2000 -3], 182:[-1 20 10 1000 -2 100 2000 200 -3], 183:[-1 20 10 1000 -2 2000 100 200 -3], 184:[-1 20 10 1000 2000 -2 100 200 -3], 185:[-1 20 10 2000 1000 -2 100 200 -3], 186:[-1 20 2000 10 1000 -2 100 200 -3], 187:[-1 2000 20 10 1000 -2 100 200 -3], 188:[2000 -1 20 10 1000 -2 100 200 -3], 189:[-1 20 1000 10 -2 100 200 -3 2000], 190:[-1 20 1000 10 -2 100 200 2000 -3], 191:[-1 20 1000 10 -2 100 2000 200 -3], 192:[-1 20 1000 10 -2 2000 100 200 -3], 193:[-1 20 1000 10 2000 -2 100 200 -3], 194:[-1 20 1000 2000 10 -2 100 200 -3], 195:[-1 20 2000 1000 10 -2 100 200 -3], 196:[-1 2000 20 1000 10 -2 100 200 -3], 197:[2000 -1 20 1000 10 -2 100 200 -3], 198:[-1 1000 20 10 -2 100 200 -3 2000], 199:[-1 1000 20 10 -2 100 200 2000 -3], 200:[-1 1000 20 10 -2 100 2000 200 -3], 201:[-1 1000 20 10 -2 2000 100 200 -3], 202:[-1 1000 20 10 2000 -2 100 200 -3], 203:[-1 1000 20 2000 10 -2 100 200 -3], 204:[-1 1000 2000 20 10 -2 100 200 -3], 205:[-1 2000 1000 20 10 -2 100 200 -3], 206:[2000 -1 1000 20 10 -2 100 200 -3], 207:[1000 -1 20 10 -2 100 200 -3 2000], 208:[1000 -1 20 10 -2 100 200 2000 -3], 209:[1000 -1 20 10 -2 100 2000 200 -3], 210:[1000 -1 20 10 -2 2000 100 200 -3], 211:[1000 -1 20 10 2000 -2 100 200 -3], 212:[1000 -1 20 2000 10 -2 100 200 -3], 213:[1000 -1 2000 20 10 -2 100 200 -3], 214:[1000 2000 -1 20 10 -2 100 200 -3], 215:[2000 1000 -1 20 10 -2 100 200 -3], 216:[-1 20 10 -2 200 100 -3 1000 2000], 217:[-1 20 10 -2 200 100 -3 2000 1000], 218:[-1 20 10 -2 200 100 2000 -3 1000], 219:[-1 20 10 -2 200 2000 100 -3 1000], 220:[-1 20 10 -2 2000 200 100 -3 1000], 221:[-1 20 10 2000 -2 200 100 -3 1000], 222:[-1 20 2000 10 -2 200 100 -3 1000], 223:[-1 2000 20 10 -2 200 100 -3 1000], 224:[2000 -1 20 10 -2 200 100 -3 1000], 225:[-1 20 10 -2 200 100 1000 -3 2000], 226:[-1 20 10 -2 200 100 1000 2000 -3], 227:[-1 20 10 -2 200 100 2000 1000 -3], 228:[-1 20 10 -2 200 2000 100 1000 -3], 229:[-1 20 10 -2 2000 200 100 1000 -3], 230:[-1 20 10 2000 -2 200 100 1000 -3], 231:[-1 20 2000 10 -2 200 100 1000 -3], 232:[-1 2000 20 10 -2 200 100 1000 -3], 233:[2000 -1 20 10 -2 200 100 1000 -3], 234:[-1 20 10 -2 200 1000 100 -3 2000], 235:[-1 20 10 -2 200 1000 100 2000 -3], 236:[-1 20 10 -2 200 1000 2000 100 -3], 237:[-1 20 10 -2 200 2000 1000 100 -3], 238:[-1 20 10 -2 2000 200 1000 100 -3], 239:[-1 20 10 2000 -2 200 1000 100 -3], 240:[-1 20 2000 10 -2 200 1000 100 -3], 241:[-1 2000 20 10 -2 200 1000 100 -3], 242:[2000 -1 20 10 -2 200 1000 100 -3], 243:[-1 20 10 -2 1000 200 100 -3 2000], 244:[-1 20 10 -2 1000 200 100 2000 -3], 245:[-1 20 10 -2 1000 200 2000 100 -3], 246:[-1 20 10 -2 1000 2000 200 100 -3], 247:[-1 20 10 -2 2000 1000 200 100 -3], 248:[-1 20 10 2000 -2 1000 200 100 -3], 249:[-1 20 2000 10 -2 1000 200 100 -3], 250:[-1 2000 20 10 -2 1000 200 100 -3], 251:[2000 -1 20 10 -2 1000 200 100 -3], 252:[-1 20 10 1000 -2 200 100 -3 2000], 253:[-1 20 10 1000 -2 200 100 2000 -3], 254:[-1 20 10 1000 -2 200 2000 100 -3], 255:[-1 20 10 1000 -2 2000 200 100 -3], 256:[-1 20 10 1000 2000 -2 200 100 -3], 257:[-1 20 10 2000 1000 -2 200 100 -3], 258:[-1 20 2000 10 1000 -2 200 100 -3], 259:[-1 2000 20 10 1000 -2 200 100 -3], 260:[2000 -1 20 10 1000 -2 200 100 -3], 261:[-1 20 1000 10 -2 200 100 -3 2000], 262:[-1 20 1000 10 -2 200 100 2000 -3], 263:[-1 20 1000 10 -2 200 2000 100 -3], 264:[-1 20 1000 10 -2 2000 200 100 -3], 265:[-1 20 1000 10 2000 -2 200 100 -3], 266:[-1 20 1000 2000 10 -2 200 100 -3], 267:[-1 20 2000 1000 10 -2 200 100 -3], 268:[-1 2000 20 1000 10 -2 200 100 -3], 269:[2000 -1 20 1000 10 -2 200 100 -3], 270:[-1 1000 20 10 -2 200 100 -3 2000], 271:[-1 1000 20 10 -2 200 100 2000 -3], 272:[-1 1000 20 10 -2 200 2000 100 -3], 273:[-1 1000 20 10 -2 2000 200 100 -3], 274:[-1 1000 20 10 2000 -2 200 100 -3], 275:[-1 1000 20 2000 10 -2 200 100 -3], 276:[-1 1000 2000 20 10 -2 200 100 -3], 277:[-1 2000 1000 20 10 -2 200 100 -3], 278:[2000 -1 1000 20 10 -2 200 100 -3], 279:[1000 -1 20 10 -2 200 100 -3 2000], 280:[1000 -1 20 10 -2 200 100 2000 -3], 281:[1000 -1 20 10 -2 200 2000 100 -3], 282:[1000 -1 20 10 -2 2000 200 100 -3], 283:[1000 -1 20 10 2000 -2 200 100 -3], 284:[1000 -1 20 2000 10 -2 200 100 -3], 285:[1000 -1 2000 20 10 -2 200 100 -3], 286:[1000 2000 -1 20 10 -2 200 100 -3], 287:[2000 1000 -1 20 10 -2 200 100 -3]", sb.String()) +} diff --git a/pkg/capigraph/layering.go b/pkg/capigraph/layering.go new file mode 100644 index 00000000..5ef3720d --- /dev/null +++ b/pkg/capigraph/layering.go @@ -0,0 +1,112 @@ +package capigraph + +import ( + "math" + "slices" +) + +func maxDistToPullSubtreeDownRecursive(subtreeRoot int16, nodeLayerMap []int, nodeToRootMap []int16, priChildrenMap [][]int16, secChildrenMap [][]int16) int { + allowedDist := math.MaxInt + for _, secChild := range secChildrenMap[subtreeRoot] { + // If this sec child has the same root as subtreeRoot - ignore it, + // we can safely push it down later, so no distance restrictions added. + // If we don't do this, we may end up in an infinite pull/push loop, see testNodeDefsPriAndSecInfinitePulldown + if nodeToRootMap[subtreeRoot] == nodeToRootMap[secChild] { + continue + } + thisLayer := nodeLayerMap[subtreeRoot] + secDependantLayer := nodeLayerMap[secChild] + maxAllowedDistCandidate := secDependantLayer - thisLayer - 1 + if maxAllowedDistCandidate < 0 { + // secChild is already at the level of subtreeRoot or even higher. + // It will be probably pushed down on the next iteration. + // Anyways, no go. + return 0 + } + if maxAllowedDistCandidate < allowedDist { + allowedDist = maxAllowedDistCandidate + } + } + + // Use primary children for recursion - we are checking a subtree of this root. + for _, priChild := range priChildrenMap[subtreeRoot] { + priChildAllowedDist := maxDistToPullSubtreeDownRecursive(priChild, nodeLayerMap, nodeToRootMap, priChildrenMap, secChildrenMap) + // Do not allow pulling down more than this child allows + if priChildAllowedDist < allowedDist { + allowedDist = priChildAllowedDist + } + } + return allowedDist +} + +func buildLayerMap(nodeDefs []NodeDef) []int { + allChildrenMap := buildAllChildrenMap(nodeDefs) + secChildrenMap := buildSecChildrenMap(nodeDefs) + priChildrenMap := buildPriChildrenMap(nodeDefs) + nodeToRootMap := buildNodeToRootMap(buildPriParentMap(nodeDefs)) + nodeLayerMap := slices.Repeat([]int{MissingLayer}, len(nodeDefs)) + // Initialize layers for root nodes + for i := range len(nodeDefs) - 1 { + nodeIdx := i + 1 + if nodeDefs[nodeIdx].PriIn.SrcId == 0 { + nodeLayerMap[nodeIdx] = 0 + } + } + + for { + reScan := false + // Push down stressed by pri or sec parent + for i := range len(nodeDefs) - 1 { + nodeIdx := int16(i + 1) + parentNodeIdx := nodeDefs[nodeIdx].PriIn.SrcId + if parentNodeIdx != 0 && nodeLayerMap[parentNodeIdx] != MissingLayer { + if nodeLayerMap[nodeIdx] == MissingLayer || nodeLayerMap[nodeIdx] <= nodeLayerMap[parentNodeIdx] { + nodeLayerMap[nodeIdx] = nodeLayerMap[parentNodeIdx] + 1 + reScan = true + } + } + for _, secParent := range nodeDefs[nodeIdx].SecIn { + parentNodeIdx := secParent.SrcId + if parentNodeIdx != 0 && nodeLayerMap[parentNodeIdx] != MissingLayer { + if nodeLayerMap[nodeIdx] == MissingLayer || nodeLayerMap[nodeIdx] <= nodeLayerMap[parentNodeIdx] { + nodeLayerMap[nodeIdx] = nodeLayerMap[parentNodeIdx] + 1 + reScan = true + } + } + } + } + + // Pull down if there is room + for i := range len(nodeDefs) - 1 { + nodeIdx := int16(i + 1) + minChildLayer := math.MaxInt + for _, childIdx := range allChildrenMap[nodeIdx] { + if nodeLayerMap[childIdx] < minChildLayer { + minChildLayer = nodeLayerMap[childIdx] + } + } + if minChildLayer < math.MaxInt { + allowedDist := minChildLayer - 1 - nodeLayerMap[nodeIdx] + if allowedDist > 0 { + nodeLayerMap[nodeIdx] += allowedDist + reScan = true + } else { + // Ok, no room between this nodeIdx and its immediate children (pri or sec). + // Can we pull the whole nodeIdx subtree down? + subtreeAllowedDist := maxDistToPullSubtreeDownRecursive(nodeIdx, nodeLayerMap, nodeToRootMap, priChildrenMap, secChildrenMap) + // It can return math.MaxInt which means we can pull down this subtree to infinity. But it does not make sense, so leave it where it is. + if subtreeAllowedDist < math.MaxInt && subtreeAllowedDist > 0 { + nodeLayerMap[nodeIdx] += subtreeAllowedDist + reScan = true + } + } + } + } + + if !reScan { + break + } + } + + return nodeLayerMap +} diff --git a/pkg/capigraph/node_def.go b/pkg/capigraph/node_def.go new file mode 100644 index 00000000..df4ce2b5 --- /dev/null +++ b/pkg/capigraph/node_def.go @@ -0,0 +1,196 @@ +package capigraph + +import ( + "fmt" + "slices" +) + +const MissingNodeId int16 = -1 +const MissingDistanceFromRootToNode int = -2 +const MissingRootSubtreeHeight int = -3 +const MissingLayer int = -4 + +type EdgeDef struct { + SrcId int16 + Text string +} + +type NodeDef struct { + Id int16 + Text string + PriIn EdgeDef + SecIn []EdgeDef + IconId string + Color int32 + Selected bool +} + +func buildPriParentMap(nodeDefs []NodeDef) []int16 { + pMap := slices.Repeat([]int16{MissingNodeId}, len(nodeDefs)) + for i, nodeDef := range nodeDefs[1:] { + nodeId := int16(i + 1) + if nodeDef.PriIn.SrcId == 0 { + pMap[nodeId] = MissingNodeId + } else { + pMap[nodeId] = nodeDefs[nodeDef.PriIn.SrcId].Id + } + } + return pMap +} + +func buildNodeToRootMap(priParentMap []int16) []int16 { + rMap := slices.Repeat([]int16{MissingNodeId}, len(priParentMap)) + for i, parentId := range priParentMap[1:] { + srcNodeId := int16(i + 1) + candidateNodeId := int16(srcNodeId) + for parentId != MissingNodeId { + candidateNodeId = parentId + parentId = priParentMap[parentId] + } + + rMap[srcNodeId] = candidateNodeId + } + return rMap +} + +func buildAllChildrenMap(nodeDefs []NodeDef) [][]int16 { + acMap := make([][]int16, len(nodeDefs)) + for i, nodeDef := range nodeDefs[1:] { + nodeId := int16(i + 1) + parentId := nodeDef.PriIn.SrcId + if parentId != 0 { + if acMap[parentId] == nil { + acMap[parentId] = make([]int16, 0, 32) + } + acMap[parentId] = append(acMap[parentId], int16(nodeId)) + } + + for _, edge := range nodeDef.SecIn { + if acMap[edge.SrcId] == nil { + acMap[edge.SrcId] = make([]int16, 0, 32) + } + acMap[edge.SrcId] = append(acMap[edge.SrcId], int16(nodeId)) + } + } + return acMap +} + +func buildSecChildrenMap(nodeDefs []NodeDef) [][]int16 { + scMap := make([][]int16, len(nodeDefs)) + for i, nodeDef := range nodeDefs[1:] { + nodeId := int16(i + 1) + for _, edge := range nodeDef.SecIn { + if scMap[edge.SrcId] == nil { + scMap[edge.SrcId] = make([]int16, 0, 32) + } + scMap[edge.SrcId] = append(scMap[edge.SrcId], int16(nodeId)) + } + } + return scMap +} + +func buildPriChildrenMap(nodeDefs []NodeDef) [][]int16 { + pcMap := make([][]int16, len(nodeDefs)) + for i, nodeDef := range nodeDefs[1:] { + nodeId := int16(i + 1) + parentId := nodeDef.PriIn.SrcId + if parentId != 0 { + if pcMap[parentId] == nil { + pcMap[parentId] = make([]int16, 0, 32) + } + pcMap[parentId] = append(pcMap[parentId], int16(nodeId)) + } + } + return pcMap +} + +func buildRootNodeList(priParentMap []int16) []int16 { + rootNodes := make([]int16, 0, 100) + for i, parentId := range priParentMap[1:] { + nodeId := int16(i + 1) + if parentId == MissingNodeId { + rootNodes = append(rootNodes, int16(nodeId)) + } + } + return rootNodes +} + +func assignDistanceFromRoot(acMap [][]int16, nodeId int16, dist int, fromOneRootToNodeDistanceMap []int) { + existingDist := fromOneRootToNodeDistanceMap[nodeId] + if existingDist < dist { + fromOneRootToNodeDistanceMap[nodeId] = dist + } + for _, childId := range acMap[nodeId] { + assignDistanceFromRoot(acMap, int16(childId), dist+1, fromOneRootToNodeDistanceMap) + } +} + +func buildRootToNodeDistanceMap(totalNodes int, rootNodes []int16, acMap [][]int16) [][]int { + fromRootToNodeDistanceMap := make([][]int, totalNodes) + for _, rootNodeId := range rootNodes { + fromRootToNodeDistanceMap[rootNodeId] = slices.Repeat([]int{MissingDistanceFromRootToNode}, len(acMap)) + assignDistanceFromRoot(acMap, rootNodeId, 0, fromRootToNodeDistanceMap[rootNodeId]) + } + return fromRootToNodeDistanceMap +} + +func getMaxSubtreeHeightByRoot(rootId int16, fromRootToNodeDistanceMap [][]int) int { + maxHeight := 0 + for _, distanceFromRoot := range fromRootToNodeDistanceMap[rootId] { + if distanceFromRoot > maxHeight { + maxHeight = distanceFromRoot + } + } + return maxHeight +} + +func buildRootSubtreeHeightsMap(totalNodes int, rootNodes []int16, rootToNodeDistanceMap [][]int) ([]int, int) { + rootSubtreeHeights := slices.Repeat([]int{MissingRootSubtreeHeight}, totalNodes) + maxSubtreeHeight := 0 + for _, rootNodeId := range rootNodes { + subtreeHeight := getMaxSubtreeHeightByRoot(rootNodeId, rootToNodeDistanceMap) + rootSubtreeHeights[rootNodeId] = subtreeHeight + if subtreeHeight > maxSubtreeHeight { + maxSubtreeHeight = subtreeHeight + } + } + return rootSubtreeHeights, maxSubtreeHeight +} + +func checkForLoopsRecursive(nodeId int16, nodeDefs []NodeDef, startNode int16) error { + if nodeDefs[nodeId].PriIn.SrcId != 0 { + if nodeDefs[nodeId].PriIn.SrcId == startNode { + return fmt.Errorf("%d<=%d", nodeId, nodeDefs[nodeId].PriIn.SrcId) + } + if err := checkForLoopsRecursive(nodeDefs[nodeId].PriIn.SrcId, nodeDefs, startNode); err != nil { + return fmt.Errorf("%d<=%s", nodeId, err.Error()) + } + } + for i := range nodeDefs[nodeId].SecIn { + edge := &(nodeDefs[nodeId].SecIn[i]) + if edge.SrcId == startNode { + return fmt.Errorf("%d<-%d", nodeId, edge.SrcId) + } + if err := checkForLoopsRecursive(edge.SrcId, nodeDefs, startNode); err != nil { + return fmt.Errorf("%d<-%s", nodeId, err.Error()) + } + } + return nil +} + +func checkNodeDef(nodeId int16, nodeDefs []NodeDef) error { + if nodeDefs[nodeId].PriIn.SrcId == 0 && len(nodeDefs[nodeId].SecIn) > 0 { + return fmt.Errorf("cannot process node def %d: it has no primary parent, but has secondary parents like %d", nodeId, nodeDefs[nodeId].SecIn[0].SrcId) + } + return checkForLoopsRecursive(nodeId, nodeDefs, nodeId) + +} + +func checkNodeIds(nodeDefs []NodeDef) error { + for i := range nodeDefs { + if nodeDefs[i].Id != int16(i) { + return fmt.Errorf("cannot process node at index %d, it has id %d", i, nodeDefs[i].Id) + } + } + return nil +} diff --git a/pkg/capigraph/node_def_test.go b/pkg/capigraph/node_def_test.go new file mode 100644 index 00000000..b9a72620 --- /dev/null +++ b/pkg/capigraph/node_def_test.go @@ -0,0 +1,34 @@ +package capigraph + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoop(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{}, []EdgeDef{{1, ""}}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{{2, ""}}, "", 0, false}, + } + assert.Equal(t, "3<=2<-1<=3", checkNodeDef(3, nodeDefs).Error()) +} + +func TestCheckBadParents(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{{2, ""}}, "", 0, false}, + {2, "2", EdgeDef{}, []EdgeDef{}, "", 0, false}, + } + assert.Equal(t, "cannot process node def 1: it has no primary parent, but has secondary parents like 2", checkNodeDef(1, nodeDefs).Error()) +} + +func TestCheckNodeIds(t *testing.T) { + nodeDefs := []NodeDef{ + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{}, []EdgeDef{}, "", 0, false}, + } + assert.Equal(t, "cannot process node at index 0, it has id 1", checkNodeIds(nodeDefs).Error()) +} diff --git a/pkg/capigraph/svg.go b/pkg/capigraph/svg.go new file mode 100644 index 00000000..8e2914fd --- /dev/null +++ b/pkg/capigraph/svg.go @@ -0,0 +1,413 @@ +package capigraph + +import ( + "fmt" + "math" + "slices" + "sort" + "strings" +) + +func DefaultNodeFontOptions() FontOptions { + return FontOptions{FontTypefaceCourier, FontWeightNormal, 20, 0.3} +} + +func DefaultEdgeLabelFontOptions() FontOptions { + return FontOptions{FontTypefaceArial, FontWeightNormal, 18, 0.3} +} + +func DefaultEdgeOptions() EdgeOptions { + return EdgeOptions{2.0} +} + +func intToCssColor(c int32) string { + colorRunes := make([]rune, 6) + colorRunes[0] = halfByteToChar(int8((c >> 20) & 0x0F)) + colorRunes[1] = halfByteToChar(int8((c >> 16) & 0x0F)) + colorRunes[2] = halfByteToChar(int8((c >> 12) & 0x0F)) + colorRunes[3] = halfByteToChar(int8((c >> 8) & 0x0F)) + colorRunes[4] = halfByteToChar(int8((c >> 4) & 0x0F)) + colorRunes[5] = halfByteToChar(int8(c & 0x0F)) + return string(colorRunes) +} + +func getColorOverride(defaultColor string, rootId int16, rootColorMap []int32) string { + if rootColorMap[rootId] == -1 { + return defaultColor + } + return intToCssColor(rootColorMap[rootId]) +} + +func getStyleColorOverrideForRoot(attrName string, rootId int16, rootColorMap []int32) string { + if rootColorMap[rootId] == -1 { + return "" + } + return fmt.Sprintf(`style="%s:#%s;"`, attrName, intToCssColor(rootColorMap[rootId])) +} +func getStyleColorOverrideForNode(attrName string, color int32) string { + if color == int32(0) { + return "" + } + return fmt.Sprintf(`style="%s:#%s;"`, attrName, intToCssColor(color)) +} + +func getStyleColorOverrideForNodeWithOpacity2(attrName string, color int32) string { + if color == int32(0) { + return "" + } + return fmt.Sprintf(`style="%s:#%s;opacity:0.3;"`, attrName, intToCssColor(color)) +} + +func drawNodeSelections(vizNodeMap []VizNode, nodeFo FontOptions) string { + sb := strings.Builder{} + for i := range len(vizNodeMap) - 1 { + curItem := vizNodeMap[i+1] + nodeX := curItem.X + curItem.TotalW/2 - curItem.NodeW/2 + if curItem.Def.Selected { + sb.WriteString(fmt.Sprintf(``+"\n", + nodeX-SelectedNodeMargin*nodeFo.SizeInPixels, curItem.Y-SelectedNodeMargin*nodeFo.SizeInPixels, curItem.NodeW+SelectedNodeMargin*nodeFo.SizeInPixels*2, curItem.NodeH+SelectedNodeMargin*nodeFo.SizeInPixels*2)) + } + } + return sb.String() + +} +func drawEdgeLines(vizNodeMap []VizNode, curItem *VizNode, nodeFo FontOptions, edgeFo FontOptions, eo EdgeOptions, rootStrokeColorMap []int32) string { + sb := strings.Builder{} + if curItem.Def != nil { + for _, edge := range curItem.Def.SecIn { + parentItem := &(vizNodeMap[edge.SrcId]) + + startX := parentItem.X + parentItem.TotalW/2.0 + endX := curItem.X + curItem.TotalW/2.0 + startOffset, endOffset := getSecOffsetX(startX, endX, nodeFo.SizeInPixels/2.0) + startX += startOffset + endX += endOffset + + startY := parentItem.Y + parentItem.NodeH + eo.StrokeWidth*2.0 + endY := curItem.Y - eo.StrokeWidth*3 + + deltaX := endX - startX + deltaY := endY - startY + + // For nearly-vertical sec connector, add a twist - so it has a better chance not to interfere with some pri connector + curveDeltaForSimilarX := 0.0 + if math.Abs(startY-endY)/math.Abs(startX-endX) > 8 { + curveDeltaForSimilarX = deltaX * math.Abs(startY-endY) / math.Abs(startX-endX) / 4 + } + sb.WriteString(fmt.Sprintf(``+"\n", + parentItem.RootId, + startX, startY, startX+deltaX*0.2, startY+deltaY*0.5, startX+deltaX*0.8+curveDeltaForSimilarX, startY+deltaY*0.5, endX, endY)) + } + } + + for _, childItem := range curItem.PriChildrenAndEnclosedRoots { + if curItem.Def != nil { + if childItem.Def.PriIn.SrcId == curItem.Def.Id { + startX := curItem.X + curItem.TotalW/2 + endX := childItem.X + childItem.TotalW/2 + sb.WriteString(fmt.Sprintf(``+"\n", + curItem.RootId, + startX, + curItem.Y+curItem.NodeH+eo.StrokeWidth*2.0, + endX, + childItem.Y-eo.StrokeWidth*3)) + } + } + sb.WriteString(drawEdgeLines(vizNodeMap, childItem, nodeFo, edgeFo, eo, rootStrokeColorMap)) + } + return sb.String() +} + +func drawNodesAndEdgeLabels(vizNodeMap []VizNode, curItem *VizNode, nodeFo FontOptions, edgeFo FontOptions, eo EdgeOptions, rootColorMap []int32) string { + xmlReplacer := strings.NewReplacer("\"", """, "'", "'", "<", "<", ">", ">", "&", "&") + sb := strings.Builder{} + if curItem.Def != nil { + title := strings.TrimSpace(xmlReplacer.Replace(fmt.Sprintf("%d %s", curItem.Def.Id, curItem.Def.IconId))) + sb.WriteString(fmt.Sprintf(``+"\n", title)) + nodeX := curItem.X + curItem.TotalW/2 - curItem.NodeW/2 + sb.WriteString(fmt.Sprintf(` `+"\n", + curItem.RootId, + getStyleColorOverrideForNodeWithOpacity2("fill", curItem.Def.Color), + nodeX, curItem.Y, curItem.NodeW, curItem.NodeH)) + sb.WriteString(fmt.Sprintf(` `+"\n", + curItem.RootId, + getStyleColorOverrideForNode("stroke", curItem.Def.Color), + nodeX, curItem.Y, curItem.NodeW, curItem.NodeH)) + actualIconSize := 0.0 + if curItem.Def.IconId != "" { + actualIconSize = curItem.NodeH - nodeFo.SizeInPixels*NodeTextDimensionMargin*2 + iconColorCssOverride := getStyleColorOverrideForRoot("fill", curItem.RootId, rootColorMap) + if curItem.Def.Color != 0 { + iconColorCssOverride = getStyleColorOverrideForNode("fill", curItem.Def.Color) + } + sb.WriteString(fmt.Sprintf(` `+"\n "+``+"\n \n", + nodeX+nodeFo.SizeInPixels*NodeTextDimensionMargin, + curItem.Y+nodeFo.SizeInPixels*NodeTextDimensionMargin, + actualIconSize/100.0, + curItem.Def.IconId, + iconColorCssOverride, + )) + } + for i, r := range strings.Split(curItem.Def.Text, "\n") { + textX := curItem.X + curItem.TotalW/2 - curItem.NodeW/2 + nodeFo.SizeInPixels*NodeTextDimensionMargin + if actualIconSize > 0.0 { + textX += actualIconSize + nodeFo.SizeInPixels*NodeTextIconInterval + } + sb.WriteString(fmt.Sprintf(` %s`+"\n", + textX, + curItem.Y+nodeFo.SizeInPixels*NodeTextDimensionMargin+float64(i)*nodeFo.SizeInPixels*(1.0+nodeFo.Interval), + xmlReplacer.Replace(r))) + } + sb.WriteString("\n") + + eolReplacer := strings.NewReplacer("\r", "", "\n", " ") + // Incoming edge labels + for _, edgeItem := range curItem.IncomingVizEdges { + // Do not draw zero-dimension label, it's a sec duplicate or just empty (pri or sec) + if edgeItem.W == 0.0 { + continue + } + sb.WriteString(fmt.Sprintf(``+"\n", strings.TrimSpace(xmlReplacer.Replace(eolReplacer.Replace(edgeItem.Edge.Text))))) + parentItem := &(vizNodeMap[edgeItem.Edge.SrcId]) + if edgeItem.HierarchyType == HierarchySec { + sb.WriteString(fmt.Sprintf(` `+"\n", + edgeItem.X, edgeItem.Y, edgeItem.W, edgeItem.H)) + sb.WriteString(fmt.Sprintf(` `+"\n", + parentItem.RootId, + edgeItem.X, edgeItem.Y, edgeItem.W, edgeItem.H)) + for i, r := range strings.Split(edgeItem.Edge.Text, "\n") { + sb.WriteString(fmt.Sprintf(` %s`+"\n", + edgeItem.X+edgeFo.SizeInPixels*LabelTextDimensionMargin, + edgeItem.Y+edgeFo.SizeInPixels*LabelTextDimensionMargin+float64(i)*edgeFo.SizeInPixels*(1+edgeFo.Interval), + xmlReplacer.Replace(r))) + } + } else if edgeItem.HierarchyType == HierarchyPri { + sb.WriteString(fmt.Sprintf(` `+"\n", + edgeItem.X, edgeItem.Y, edgeItem.W, edgeItem.H)) + sb.WriteString(fmt.Sprintf(` `+"\n", + parentItem.RootId, + edgeItem.X, edgeItem.Y, edgeItem.W, edgeItem.H)) + for i, r := range strings.Split(edgeItem.Edge.Text, "\n") { + sb.WriteString(fmt.Sprintf(` %s`+"\n", + edgeItem.X+edgeFo.SizeInPixels*LabelTextDimensionMargin, + edgeItem.Y+edgeFo.SizeInPixels*LabelTextDimensionMargin+float64(i)*edgeFo.SizeInPixels*(1+edgeFo.Interval), + xmlReplacer.Replace(r))) + } + } + sb.WriteString("\n") + } + } + + for _, childItem := range curItem.PriChildrenAndEnclosedRoots { + sb.WriteString(drawNodesAndEdgeLabels(vizNodeMap, childItem, nodeFo, edgeFo, eo, rootColorMap)) + } + return sb.String() +} + +type EdgeOptions struct { + StrokeWidth float64 +} + +func DefaultPalette() []int32 { + // return []int32{0x4E79A7, 0xF28E2C, 0xE15759, 0x76B7B2, 0x59A14F, 0xEDC949, 0xAF7AA1, 0xFF9DA7, 0x9C755F, 0xBAB0A} // New T10 + // return []int32{0x4C72B0, 0xDD8452, 0x55A868, 0xC44E52, 0x8172B3, 0x937860, 0xDA8BC3, 0x8C8C8C, 0xCCB974, 0x64B5CD} // deep + // return []int32{0xA1C9F4, 0xFFB482, 0x8DE5A1, 0xFF9F9B, 0xD0BBFF, 0xDEBB9B, 0xFAB0E4, 0xCFCFCF, 0xFFFEA3, 0xB9F2F0} // pastel, too bleached + return []int32{0x023EFF, 0xFF7C00, 0x1AC938, 0xE8000B, 0x8B2BE2, 0x9F4800, 0xF14CC1, 0xA3A3A3, 0xFFC400, 0x00D7FF} // bright, very visible +} + +// Currently not used +// func getTextColorForBackground(bgColor int32) int32 { +// r := (bgColor >> 16) & 0xFF +// g := (bgColor >> 8) & 0xFF +// b := bgColor & 0xFF +// uicolors := []float64{float64(r / 255.0), float64(g / 255.0), float64(b / 255.0)} +// c := make([]float64, len(uicolors)) +// for i, col := range uicolors { +// if col <= 0.03928 { +// c[i] = col / 12.92 +// } else { +// c[i] = math.Pow((col+0.055)/1.055, 2.4) +// } +// } +// l := (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]) +// if l > 0.179 { +// return 0 +// } else { +// return 0x00FFFFFF +// } +// } + +func buildRootColorMap(vizNodeMap []VizNode, palette []int32) []int32 { + rootIds := make([]int16, 0, len(vizNodeMap)) + rootCountMap := make([]int, len(vizNodeMap)) + for i := range len(vizNodeMap) - 1 { + vizNode := &vizNodeMap[i+1] + if rootCountMap[vizNode.RootId] == 0 { + rootIds = append(rootIds, vizNode.RootId) + } + rootCountMap[vizNode.RootId]++ + } + sort.Slice(rootIds, func(i, j int) bool { + return rootCountMap[rootIds[i]] > rootCountMap[rootIds[j]] + }) + + rootColorMap := slices.Repeat([]int32{-1}, len(vizNodeMap)) + if len(palette) > 0 { + colorCounter := 0 + for _, rootId := range rootIds { + rootColorMap[rootId] = palette[colorCounter%len(palette)] + colorCounter++ + } + } + return rootColorMap +} + +const SelectedNodeMargin float64 = 0.0 + +func draw(vizNodeMap []VizNode, nodeFo FontOptions, edgeFo FontOptions, eo EdgeOptions, defsXml string, css string, palette []int32, totalPermutations int64, elapsed float64, bestDist float64) string { + topCoord := math.MaxFloat64 + bottomCoord := -math.MaxFloat64 + minLeft := math.MaxFloat64 + maxRight := -math.MaxFloat64 + for i := range len(vizNodeMap) - 1 { + hi := &vizNodeMap[i+1] + for _, edge := range hi.IncomingVizEdges { + if edge.X < minLeft { + minLeft = edge.X + } + if edge.X+edge.W > maxRight { + maxRight = edge.X + edge.W + } + } + nodeLeft := hi.X + nodeRight := hi.X + hi.NodeW + nodeTop := hi.Y + nodeBottom := hi.Y + hi.NodeH + if hi.Def.Selected { + nodeLeft -= SelectedNodeMargin * nodeFo.SizeInPixels + nodeRight += SelectedNodeMargin * nodeFo.SizeInPixels + nodeTop -= SelectedNodeMargin * nodeFo.SizeInPixels + nodeBottom += SelectedNodeMargin * nodeFo.SizeInPixels + } + if nodeLeft < minLeft { + minLeft = nodeLeft + } + if nodeRight > maxRight { + maxRight = nodeRight + } + if nodeBottom > bottomCoord { + bottomCoord = nodeBottom + } + if nodeTop < topCoord { + topCoord = nodeTop + } + } + + vbLeft := int(minLeft - 10.0) + vbRight := int(maxRight + 20.0) + vbTop := int(topCoord - 10.0) + vbBottom := int(bottomCoord + 20.0) + + rootColorMap := buildRootColorMap(vizNodeMap, palette) + + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf( + ``+"\n", vbLeft, vbTop, vbRight-vbLeft, vbBottom-vbTop)) + sb.WriteString("\n") + + for rootId := range len(rootColorMap) { + sb.WriteString(fmt.Sprintf(``+"\n", + rootId, + getColorOverride("000000", int16(rootId), rootColorMap))) + } + + // Caller-provided defs (icons etc) + sb.WriteString(defsXml) + sb.WriteString("\n") + sb.WriteString("\n") + sb.WriteString(fmt.Sprintf(``+"\n", vbLeft, vbTop, vbRight-vbLeft, vbBottom-vbTop)) + + // Node selections at the z-bottom + sb.WriteString(drawNodeSelections(vizNodeMap, nodeFo)) + + // Edge lines first, nodes and labels can overlap with them + topItem := &vizNodeMap[0] + sb.WriteString(drawEdgeLines(vizNodeMap, topItem, nodeFo, edgeFo, eo, rootColorMap)) + // Nodes and labels + sb.WriteString(drawNodesAndEdgeLabels(vizNodeMap, topItem, nodeFo, edgeFo, eo, rootColorMap)) + + sb.WriteString(fmt.Sprintf(`Perms %d, elapsed %.3fs, dist %.1f`+"\n", totalPermutations, elapsed, bestDist)) + + sb.WriteString("\n") + return sb.String() +} + +func DrawOptimized(nodeDefs []NodeDef, nodeFo FontOptions, edgeFo FontOptions, edgeOptions EdgeOptions, defsOverride string, cssOverride string, palette []int32) (string, []VizNode, int64, float64, float64, error) { + if err := checkNodeIds(nodeDefs); err != nil { + return "", nil, int64(0), 0.0, 0.0, err + } + + for i := range len(nodeDefs) - 1 { + if err := checkNodeDef(int16(i+1), nodeDefs); err != nil { + return "", nil, int64(0), 0.0, 0.0, err + } + } + vizNodeMap, totalPermutations, elapsed, bestDist, err := getBestHierarchy(nodeDefs, nodeFo, edgeFo, true) + if err != nil { + return "", nil, int64(0), 0.0, 0.0, err + } + svgString := draw(vizNodeMap, nodeFo, edgeFo, edgeOptions, defsOverride, cssOverride, palette, totalPermutations, elapsed, bestDist) + return svgString, vizNodeMap, totalPermutations, elapsed, bestDist, nil +} + +func DrawUnoptimized(nodeDefs []NodeDef, nodeFo FontOptions, edgeFo FontOptions, edgeOptions EdgeOptions, defsOverride string, cssOverride string, palette []int32) (string, []VizNode, error) { + if err := checkNodeIds(nodeDefs); err != nil { + return "", nil, err + } + + for i := range len(nodeDefs) - 1 { + if err := checkNodeDef(int16(i+1), nodeDefs); err != nil { + return "", nil, err + } + } + + vizNodeMap, totalPermutations, elapsed, bestDist, err := getBestHierarchy(nodeDefs, nodeFo, edgeFo, false) + if err != nil { + return "", nil, err + } + svgString := draw(vizNodeMap, nodeFo, edgeFo, edgeOptions, defsOverride, cssOverride, palette, totalPermutations, elapsed, bestDist) + return svgString, vizNodeMap, nil +} diff --git a/pkg/capigraph/svg_icons.go b/pkg/capigraph/svg_icons.go new file mode 100644 index 00000000..52728a0d --- /dev/null +++ b/pkg/capigraph/svg_icons.go @@ -0,0 +1,107 @@ +package capigraph + +const CapillariesIcons100x100 = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` diff --git a/pkg/capigraph/svg_icons_test.svg b/pkg/capigraph/svg_icons_test.svg new file mode 100644 index 00000000..ae89859c --- /dev/null +++ b/pkg/capigraph/svg_icons_test.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/capigraph/svg_test.go b/pkg/capigraph/svg_test.go new file mode 100644 index 00000000..a75b48de --- /dev/null +++ b/pkg/capigraph/svg_test.go @@ -0,0 +1,936 @@ +package capigraph + +import ( + "fmt" + "math" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Common MxPermutator and SVG tests + +func TestBasicSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsBasic, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 72.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TrivialParallelSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsTrivialParallel, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 47.2, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestOneEnclosingOneLevelSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsOneEnclosedOneLevel, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(6), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestOneEnclosedTwoLevelsSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsOneEnclosedTwoLevels, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(6), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestNoIntervalsSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsNoIntervals, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 0.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestFlat10Svg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsFlat10, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(3628800), totalPermutations) + assert.Equal(t, 0.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestTwoEnclosingTwoLevelsNodeSizeMattersSvg(t *testing.T) { + // Only one of 8, 9 is enclosed + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsTwoEnclosedNodeSizeMatters, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(24), totalPermutations) + assert.Equal(t, 432.0, bestDist) + fmt.Printf("%s\n", svg) + + // Now make nodes 4 and 5 considerably wider - it will change the best hierarchy, 8 and 9 enclosed + testNodeDefsTwoEnclosedNodeSizeMatters[4].Text += " wider" + testNodeDefsTwoEnclosedNodeSizeMatters[5].Text += " wider" + + svg, _, _, _, bestDist, _ = DrawOptimized(testNodeDefsTwoEnclosedNodeSizeMatters, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, 576.0, math.Round(bestDist*100.0)/100.0) + fmt.Printf("%s\n", svg) +} + +func TestOneSecondarySvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsOneSecondary, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(6), totalPermutations) + assert.Equal(t, 72.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestDiamonSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsDiamond, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(24), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestSubtreeBelowLongSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsSubtreeBelowLong, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 72.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestOneNotTwoLevelsDownSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsOneNotTwoLevelsDown, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestMultiSecParentPullDownSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsMultiSecParentPullDown, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestMultiSecParentNoPullDownSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsMultiSecParentNoPullDown, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestTwoLevelsFromOneParentSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsTwoLevelsFromOneParent, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestTwoLevelsFromOneParentSameRootSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsTwoLevelsFromOneParentSameRoot, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 72.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestTwoLevelsFromOneParentSameRootTwoFakesSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsTwoLevelsFromOneParentSameRootTwoFakes, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 72.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestDuplicateSecLabelsSvg(t *testing.T) { + svg, vizNodeMap, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsDuplicateSecLabels, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(720), totalPermutations) + assert.Equal(t, 660.0, bestDist) + // Zero-width sec labels to avoid interlapped + assert.Equal(t, 0.0, vizNodeMap[8].IncomingVizEdges[1].W) + // Displayed sec labels + assert.Equal(t, 37.08, vizNodeMap[7].IncomingVizEdges[1].W) + assert.Equal(t, 37.08, vizNodeMap[9].IncomingVizEdges[1].W) + assert.Equal(t, 37.08, vizNodeMap[10].IncomingVizEdges[1].W) + assert.Equal(t, 37.08, vizNodeMap[11].IncomingVizEdges[1].W) + fmt.Printf("%s\n", svg) +} + +func TestLayerLongRootsSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsLayerLongRoots, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(6), totalPermutations) + assert.Equal(t, 216.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestPriAndSecInfinitePulldownSvg(t *testing.T) { + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefsPriAndSecInfinitePulldown, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(4), totalPermutations) + assert.Equal(t, 144.0, bestDist) + fmt.Printf("%s\n", svg) +} + +// Takes 15 seconds, disable for quick testing +// func Test40milPermsSvg(t *testing.T) { +// defer profile.Start(profile.CPUProfile).Stop() +// svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(testNodeDefs40milPerms, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) +// assert.Equal(t, int64(41472000), totalPermutations) +// assert.Equal(t, 84.0, math.Round(bestDist*100.0)/100.0) +// fmt.Printf("%s\n", svg) +// } + +func Test300bilPermsSvg(t *testing.T) { + _, _, _, _, _, err := DrawOptimized(testNodeDefs300bilPerms, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Contains(t, err.Error(), "313528320000") + svg, _, err := DrawUnoptimized(testNodeDefs300bilPerms, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, nil, err) + fmt.Printf("%s\n", svg) +} + +// SVG-specific tests + +// Unoptimized only, too many permutations +func TestUnoptimizedSvg(t *testing.T) { + nodeDefs := make([]NodeDef, 0, 10000) + nodeDefs = append(nodeDefs, NodeDef{0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}) + var populateChildren func(parentIdx int, firstChildIdx int, layer int) int + populateChildren = func(parentIdx int, firstChildIdx int, layer int) int { + if layer == 4 { + return firstChildIdx + } + nextChildIdx := firstChildIdx + for range 5 - layer { + parentOverrideIdx := parentIdx + if firstChildIdx%7 == 0 { + parentOverrideIdx = 0 + } + newNode := NodeDef{int16(nextChildIdx), fmt.Sprintf("%d", nextChildIdx), EdgeDef{int16(parentOverrideIdx), ""}, []EdgeDef{}, "", 0, false} + if parentOverrideIdx != 0 && firstChildIdx%9 == 0 { + newNode.SecIn = append(newNode.SecIn, EdgeDef{int16(firstChildIdx / 2), ""}) + } + nodeDefs = append(nodeDefs, newNode) + nextChildIdx = populateChildren(nextChildIdx, nextChildIdx+1, layer+1) + } + return nextChildIdx + } + populateChildren(0, 1, 0) + + svg, _, err := DrawUnoptimized(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, nil, err) + fmt.Printf("%s\n", svg) +} + +// Takes 160s to complete, but working. Fake nodes, enclosed subtrees, 20 levels, 484 nodes. +// func TestInsanelyBigBinaryTreeSvg(t *testing.T) { +// nodeDefs := make([]NodeDef, 0, 10000) +// nodeDefs = append(nodeDefs, NodeDef{0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}) +// var populateChildren func(parentIdx int, firstChildIdx int, layer int) int +// populateChildren = func(parentIdx int, firstChildIdx int, layer int) int { +// if layer == 7 { +// return firstChildIdx +// } +// nextChildIdx := firstChildIdx +// for range 2 { +// parentOverrideIdx := parentIdx +// if firstChildIdx%7 == 0 && firstChildIdx < 20 { +// parentOverrideIdx = 0 +// layer = 0 +// } +// newNode := NodeDef{int16(nextChildIdx), fmt.Sprintf("%d", nextChildIdx), EdgeDef{int16(parentOverrideIdx), ""}, []EdgeDef{}, "", 0, false} +// if parentOverrideIdx != 0 && firstChildIdx%9 == 0 { +// newNode.SecIn = append(newNode.SecIn, EdgeDef{int16(firstChildIdx / 2), ""}) +// } +// nodeDefs = append(nodeDefs, newNode) +// nextChildIdx = populateChildren(nextChildIdx, nextChildIdx+1, layer+1) +// } +// return nextChildIdx +// } +// populateChildren(0, 1, 0) + +// svg, _, totalPermutations, _, _, err := DrawOptimized(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) +// assert.Equal(t, int64(3819584), totalPermutations) +// assert.Equal(t, nil, err) +// fmt.Printf("%s\n", svg) +// } + +func TestEnclosingOneLevelWideNodes(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "A1\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "A21\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{1, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A1 to A21"}, []EdgeDef{}, "", 0, false}, + {3, "A22\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{1, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A1 to A22"}, []EdgeDef{}, "", 0, false}, + {4, "A31\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{2, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A21 to A31"}, []EdgeDef{{6, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom B1 to A31"}}, "", 0, false}, + {5, "A32\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{3, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A22 to A32"}, []EdgeDef{{6, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom B1 to A32"}}, "", 0, false}, + {6, "B1\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{}, []EdgeDef{}, "", 0, false}, + } + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(6), totalPermutations) + assert.Equal(t, 768.0, bestDist) + fmt.Printf("%s\n", svg) +} + +func TestHalfComplexWithEnclosed(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "A1\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "A2\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{1, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A1 to A2"}, []EdgeDef{}, "", 0, false}, + {3, "A31\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{2, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A2 to A31"}, []EdgeDef{{10, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom B2 to A31"}}, "", 0, false}, + {4, "A32\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{2, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A2 to A32"}, []EdgeDef{{14, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom C3 to A32"}}, "", 0, false}, + {5, "A41\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{4, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A32 to A41"}, []EdgeDef{{11, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom B3 to A41"}}, "", 0, false}, + {6, "A42\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{4, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A32 to A42"}, []EdgeDef{{14, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom C3 to A42"}}, "", 0, false}, + {7, "A51\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{5, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A41 to A51"}, []EdgeDef{{15, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom D1 to A51"}}, "", 0, false}, + {8, "A52\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{6, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom A42 to A52"}, []EdgeDef{{15, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom D1 to A52"}}, "", 0, false}, + {9, "B1\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {10, "B2\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{9, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom B1 to B2"}, []EdgeDef{}, "", 0, false}, + {11, "B3\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{10, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom B2 to B3"}, []EdgeDef{}, "", 0, false}, + {12, "C1\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {13, "C2\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{12, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom C1 to C2"}, []EdgeDef{}, "", 0, false}, + {14, "C3\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{13, "Lorem\n\n\nipsum\ndolor\nsit\namet\nfrom C2 to C3"}, []EdgeDef{}, "", 0, false}, + {15, "D1\nlorem ipsum dolor sit amet,\nconsectetur adipisci elit,\nsed eiusmod tempor incidunt\nut labore\net dolore magna aliqua", EdgeDef{}, []EdgeDef{}, "", 0, false}, + } + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(48), totalPermutations) + assert.Equal(t, 3072.0, math.Round(bestDist)) + fmt.Printf("%s\n", svg) +} + +func TestConflictingSecAndTotalViewboxWidthAdjustedToLabel(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "A", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "B", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {3, "C", EdgeDef{1, "A to C"}, []EdgeDef{{2, "B to ? duplicate going really wide"}}, "", 0, false}, + {4, "D", EdgeDef{3, "C to D"}, []EdgeDef{{2, "B to ? duplicate going really wide"}}, "", 0, false}, + } + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, int64(2), totalPermutations) + assert.Equal(t, 144.0, math.Round(bestDist*100.0)/100.0) + fmt.Printf("%s\n", svg) +} + +func TestCapillariesIcons(t *testing.T) { + nodeDefs := []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + { + 1, + "01_read_payments\n" + + "Read from files into a table\n" + + "Files: /tmp/capi_in/.../CAS_2023_R08_G1_20231020_000.parquet\n" + + "Table created: payments", + EdgeDef{}, + []EdgeDef{}, + "icon-database-table-read", + 0, + true, + }, + { + 2, + "02_loan_ids\n" + + "Select distinct rows\n" + + "Index used: unique(loan_id)\n" + + "Table created: loan_ids", + EdgeDef{1, "payments\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + true, + }, + { + 3, + "02_deal_names\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name)\n" + + "Table created: deal_names", + EdgeDef{2, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + true, + }, + { + 4, + "02_deal_sellers\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name, seller_name)\n" + + "Table created: deal_sellers", + EdgeDef{2, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + true, + }, + { + 5, + "03_deal_total_upbs\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_total_upbs", + EdgeDef{3, "deal_names\n(10 batches)"}, + []EdgeDef{{2, "idx_loan_ids_deal_name\n(lookup)"}}, + "icon-database-table-join", + 0, + true, + }, + { + 6, + "04_loan_payment_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: loan_payment_summaries", + EdgeDef{2, "loan_ids\n(10 batches)"}, + []EdgeDef{{1, "idx_payments_by_loan_id\n(lookup)"}}, + "icon-database-table-join", + 0, + true, + }, + { + 7, + "04_loan_summaries_calculated\n" + + "Apply Python calculations\n" + + "Group: true, join: left\n" + + "Table created: loan_summaries_calculated", + EdgeDef{6, "loan_payment_summaries\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-py", + 0, + true, + }, + { + 8, + "05_deal_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_summaries", + EdgeDef{5, "deal_total_upbs\n(10 batches)"}, + []EdgeDef{{7, "idx_loan_summaries_calculated_deal_name\n(lookup)\ndeal_name"}}, + "icon-database-table-join", + 0, + true, + }, + { + 9, + "05_deal_seller_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_seller_summaries", + EdgeDef{4, "deal_sellers\n(10 batches)"}, + []EdgeDef{{7, "idx_loan_summaries_calculated_deal_name\n(lookup)\ndeal_name\nseller_name"}}, + "icon-database-table-join", + 0, + true, + }, + { + 10, + "04_write_file_loan_summaries_calculated\n" + + "Write from table to files\n" + + "Table: loan_summaries_calculated\n" + + "Files: /tmp/.../.../loan_summaries_calculated.parquet", + EdgeDef{7, "loan_summaries_calculated\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + true, + }, + { + 11, + "05_write_file_deal_summaries\n" + + "Write from table to files\n" + + "Table: deal_summaries\n" + + "Files: /tmp/.../.../deal_summaries.parquet", + EdgeDef{8, "deal_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + true, + }, + { + 12, + "05_write_file_deal_seller_summaries\n" + + "Write from table to files\n" + + "Table: deal_seller_summaries\n" + + "Files: /tmp/.../.../deal_seller_summaries.parquet", + EdgeDef{9, "deal_seller_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + true, + }, + + { + 13, + "01_read_payments\n" + + "Read from files into a table\n" + + "Files: /tmp/capi_in/.../CAS_2023_R08_G1_20231020_000.parquet\n" + + "Table created: payments", + EdgeDef{}, + []EdgeDef{}, + "icon-database-table-read", + 0, + false, + }, + { + 14, + "02_loan_ids\n" + + "Select distinct rows\n" + + "Index used: unique(loan_id)\n" + + "Table created: loan_ids", + EdgeDef{13, "payments\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 15, + "02_deal_names\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name)\n" + + "Table created: deal_names", + EdgeDef{14, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 16, + "02_deal_sellers\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name, seller_name)\n" + + "Table created: deal_sellers", + EdgeDef{14, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 17, + "03_deal_total_upbs\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_total_upbs", + EdgeDef{15, "deal_names\n(10 batches)"}, + []EdgeDef{{14, "idx_loan_ids_deal_name\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 18, + "04_loan_payment_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: loan_payment_summaries", + EdgeDef{14, "loan_ids\n(10 batches)"}, + []EdgeDef{{13, "idx_payments_by_loan_id\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 19, + "04_loan_summaries_calculated\n" + + "Apply Python calculations\n" + + "Group: true, join: left\n" + + "Table created: loan_summaries_calculated", + EdgeDef{18, "loan_payment_summaries\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-py", + 0, + false, + }, + { + 20, + "05_deal_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_summaries", + EdgeDef{17, "deal_total_upbs\n(10 batches)"}, + []EdgeDef{{19, "idx_loan_summaries_calculated_deal_name\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 21, + "05_deal_seller_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_seller_summaries", + EdgeDef{16, "deal_sellers\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-join", + 0, + false, + }, + { + 22, + "04_write_file_loan_summaries_calculated\n" + + "Write from table to files\n" + + "Table: loan_summaries_calculated\n" + + "Files: /tmp/.../.../loan_summaries_calculated.parquet", + EdgeDef{19, "loan_summaries_calculated\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + { + 23, + "05_write_file_deal_summaries\n" + + "Write from table to files\n" + + "Table: deal_summaries\n" + + "Files: /tmp/.../.../deal_summaries.parquet", + EdgeDef{20, "deal_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + { + 24, + "05_write_file_deal_seller_summaries\n" + + "Write from table to files\n" + + "Table: deal_seller_summaries\n" + + "Files: /tmp/.../.../deal_seller_summaries.parquet", + EdgeDef{21, "deal_seller_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + + { + 25, + "01_read_payments\n" + + "Read from files into a table\n" + + "Files: /tmp/capi_in/.../CAS_2023_R08_G1_20231020_000.parquet\n" + + "Table created: payments", + EdgeDef{}, + []EdgeDef{}, + "icon-database-table-read", + 0, + false, + }, + { + 26, + "02_loan_ids\n" + + "Select distinct rows\n" + + "Index used: unique(loan_id)\n" + + "Table created: loan_ids", + EdgeDef{25, "payments\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 27, + "02_deal_names\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name)\n" + + "Table created: deal_names", + EdgeDef{26, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 28, + "02_deal_sellers\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name, seller_name)\n" + + "Table created: deal_sellers", + EdgeDef{26, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 29, + "03_deal_total_upbs\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_total_upbs", + EdgeDef{27, "deal_names\n(10 batches)"}, + []EdgeDef{{26, "idx_loan_ids_deal_name\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 30, + "04_loan_payment_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: loan_payment_summaries", + EdgeDef{26, "loan_ids\n(10 batches)"}, + []EdgeDef{{25, "idx_payments_by_loan_id\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 31, + "04_loan_summaries_calculated\n" + + "Apply Python calculations\n" + + "Group: true, join: left\n" + + "Table created: loan_summaries_calculated", + EdgeDef{30, "loan_payment_summaries\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-py", + 0, + false, + }, + { + 32, + "05_deal_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_summaries", + EdgeDef{29, "deal_total_upbs\n(10 batches)"}, + []EdgeDef{{31, "idx_loan_summaries_calculated_deal_name\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 33, + "05_deal_seller_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_seller_summaries", + EdgeDef{28, "deal_sellers\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-join", + 0, + false, + }, + { + 34, + "04_write_file_loan_summaries_calculated\n" + + "Write from table to files\n" + + "Table: loan_summaries_calculated\n" + + "Files: /tmp/.../.../loan_summaries_calculated.parquet", + EdgeDef{31, "loan_summaries_calculated\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + { + 35, + "05_write_file_deal_summaries\n" + + "Write from table to files\n" + + "Table: deal_summaries\n" + + "Files: /tmp/.../.../deal_summaries.parquet", + EdgeDef{32, "deal_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + { + 36, + "05_write_file_deal_seller_summaries\n" + + "Write from table to files\n" + + "Table: deal_seller_summaries\n" + + "Files: /tmp/.../.../deal_seller_summaries.parquet", + EdgeDef{33, "deal_seller_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + + { + 37, + "01_read_payments\n" + + "Read from files into a table\n" + + "Files: /tmp/capi_in/.../CAS_2023_R08_G1_20231020_000.parquet\n" + + "Table created: payments", + EdgeDef{}, + []EdgeDef{}, + "icon-database-table-read", + 0, + false, + }, + { + 38, + "02_loan_ids\n" + + "Select distinct rows\n" + + "Index used: unique(loan_id)\n" + + "Table created: loan_ids", + EdgeDef{37, "payments\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 39, + "02_deal_names\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name)\n" + + "Table created: deal_names", + EdgeDef{38, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 40, + "02_deal_sellers\n" + + "Select distinct rows\n" + + "Index used: unique(deal_name, seller_name)\n" + + "Table created: deal_sellers", + EdgeDef{38, "loan_ids\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-distinct", + 0, + false, + }, + { + 41, + "03_deal_total_upbs\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_total_upbs", + EdgeDef{39, "deal_names\n(10 batches)"}, + []EdgeDef{{38, "idx_loan_ids_deal_name\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 42, + "04_loan_payment_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: loan_payment_summaries", + EdgeDef{38, "loan_ids\n(10 batches)"}, + []EdgeDef{{37, "idx_payments_by_loan_id\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 43, + "04_loan_summaries_calculated\n" + + "Apply Python calculations\n" + + "Group: true, join: left\n" + + "Table created: loan_summaries_calculated", + EdgeDef{42, "loan_payment_summaries\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-py", + 0, + false, + }, + { + 44, + "05_deal_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_summaries", + EdgeDef{41, "deal_total_upbs\n(10 batches)"}, + []EdgeDef{{43, "idx_loan_summaries_calculated_deal_name\n(lookup)"}}, + "icon-database-table-join", + 0, + false, + }, + { + 45, + "05_deal_seller_summaries\n" + + "Join with lookup table\n" + + "Group: true, join: left\n" + + "Table created: deal_seller_summaries", + EdgeDef{40, "deal_sellers\n(10 batches)"}, + []EdgeDef{}, + "icon-database-table-join", + 0, + false, + }, + { + 46, + "04_write_file_loan_summaries_calculated\n" + + "Write from table to files\n" + + "Table: loan_summaries_calculated\n" + + "Files: /tmp/.../.../loan_summaries_calculated.parquet", + EdgeDef{43, "loan_summaries_calculated\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + { + 47, + "05_write_file_deal_summaries\n" + + "Write from table to files\n" + + "Table: deal_summaries\n" + + "Files: /tmp/.../.../deal_summaries.parquet", + EdgeDef{44, "deal_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + { + 48, + "05_write_file_deal_seller_summaries\n" + + "Write from table to files\n" + + "Table: deal_seller_summaries\n" + + "Files: /tmp/.../.../deal_seller_summaries.parquet", + EdgeDef{45, "deal_seller_summaries\n(no parallelism)"}, + []EdgeDef{}, + "icon-parquet", + 0, + false, + }, + } + // overrideCss := ".rect-node-background {rx:20; ry:20;} .rect-node {rx:20; ry:20;} .capigraph-rendering-stats {fill:black;}" + // nodeColorMap := []int32{0x010101, 0x0000FF, 0x008000, 0xFF0000, 0xFF8C00, 0x2F4F4F} //none, blue, darkgreen, red, darkorange, darkslategray (none, start, success, fail, stopreceived, unknown) + // for nodeIdx := range nodeDefs { + // nodeDefs[nodeIdx].Color = nodeColorMap[nodeIdx%len(nodeColorMap)] + // } + svg, _, totalPermutations, _, bestDist, _ := DrawOptimized(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), CapillariesIcons100x100, "" /* overrideCss*/, DefaultPalette()) + assert.Equal(t, int64(31104), totalPermutations) + assert.Equal(t, 6618.0, math.Round(bestDist*100.0)/100.0) + fmt.Printf("%s\n", svg) +} + +func TestPrefixTree(t *testing.T) { + rootWord := "trip" + allWords := []string{"tri", "trip", "trim", "trio", "trib", "trig", "trix", "trin", "trid", "trit", "trial", "tried", "trips", "tries", "trick", "tribe", "trina", "triad", "trims", "trier", "trike", "trios", "trice", "trine", "trite", "tripe", "trill", "triac", "trias", "trigo", "triga", "trist", "trica", "trixy", "trior", "tripl", "triol", "tript", "tripy", "trink", "trigs", "trifa", "trials", "triple", "tricks", "tribal", "trivia", "tribes", "tripod", "tricky", "triton", "tricia", "trisha", "trixie", "triage", "trifle", "trichy", "tricot", "triads", "trivet", "triste", "triumf", "triune", "triode", "trigon", "tringa", "tripos", "trilby", "trimer", "triply", "trills", "tripel", "trillo", "triers", "tricon", "triops", "trines", "trifid", "trices", "triose", "triols", "triole", "triwet", "trivat", "tripot", "trityl", "tripes", "tritor", "tripla", "tricae", "triter", "trisul", "tripal", "trinol", "trifly", "triens", "triene", "triduo", "tridra", "tridii", "triace", "trichi", "triced", "tricar", "triazo", "triary", "trigae", "trigly", "trinil", "trigla", "trined", "trinal", "trimly", "trilli", "trilit", "trikir", "triker", "trijet", "trigyn", "trigos", "triact", "tribune", "trigger", "tribute", "trinity", "triumph", "trilogy", "trivial", "trivium", "trimmed", "tristan", "trimmer", "tripods", "trident", "triples", "trickle", "tricked", "triplet", "tritium", "tripoli", "tripled", "trinket", "tripped", "triplex", "tripple", "triceps", "trisomy", "tripper", "tribble", "trifles", "trifold", "trianon", "trivets", "trigram", "tritone", "tripler", "triadic", "tritons", "tricker", "triable", "tripack", "trimers", "trifled", "trionfo", "tricorn", "trinkle", "triller", "tribuna", "trireme", "tristam", "trilled", "triodes", "triduum", "triolet", "trionfi", "triplum", "trionyx", "tripart", "trionym", "triurid", "triones", "trioxid", "trioses", "trional", "tripara", "tripery", "tripody", "tritely", "tritest", "tritish", "tritolo", "tritoma", "triture", "triunal", "triunes", "triuris", "trivant", "trivvet", "trizoic", "tritaph", "trisulc", "trisula", "trippet", "tripsis", "triquet", "trisalt", "trisazo", "trisect", "triseme", "trishaw", "trishna", "trismic", "trismus", "trisome", "trizone", "trioecs", "triacid", "tribrac", "tribual", "trichia", "tricing", "trickie", "trickly", "tricksy", "triclad", "tricots", "triduam", "triduan", "triedly", "trienes", "trifler", "triblet", "tribase", "triaene", "triages", "triakid", "triamid", "triamin", "triarch", "triarii", "triaryl", "triatic", "triaxal", "triaxon", "triazin", "tribade", "tribady", "triflet", "trifoil", "trifoly", "trillet", "trillil", "trilobe", "trimera", "trinary", "trindle", "trinely", "tringle", "trining", "trinkum", "trinode", "trintle", "triobol", "triodia", "trilium", "trilith", "triform", "trigamy", "trigged", "triglid", "triglot", "trigona", "trigone", "trigons", "trijets", "trikaya", "triketo", "trilabe", "trilisa", "trilite", "triodon", "trinidad", "triangle", "tribunal", "triggers", "trillion", "trimming", "tripping", "tributes", "trimmers", "triumphs", "tristate", "triplets", "tricycle", "trillium", "tripwire", "tristram", "trinkets", "trickery", "tribulus", "triassic", "trifecta", "tricking", "triptych", "trifling", "tricolor", "tributed", "triticum", "trickier", "tripling", "trickled", "trioxide", "triaxial", "triazine", "trilling", "trickles", "trippers", "triskele", "trigonal", "triploid", "tristeza", "trimaran", "tribally", "tribunes", "trifocal", "triazole", "trigrams", "triology", "trimeric", "triumvir", "triplane", "trialist", "triatoma", "triturus", "tripolis", "trichome", "trivette", "trimurti", "tringoid", "trinerve", "triplite", "triplice", "trindled", "tripodal", "triplopy", "trindles", "tringine", "triphora", "triphony", "trinodal", "trinkums", "trinklet", "trioecia", "trioleic", "triolein", "triolets", "tripenny", "trioxids", "tripacks", "trinkety", "tripedal", "tripeman", "triodion", "triphane", "trinitro", "triphase", "trinomen", "trizonia", "tritomas", "tritiums", "triticin", "tritical", "tritiate", "trithing", "tristyly", "tristive", "tristich", "tristful", "tritonal", "tritones", "trizonal", "trizomal", "trivirga", "trivalve", "triunity", "triunion", "tritural", "tritoral", "tritonic", "tritonia", "triposes", "trispast", "triptote", "triptane", "tripsome", "tripsill", "trippler", "trippist", "trippets", "trippant", "tripoter", "tripolar", "triptyca", "tripudia", "trisomic", "trisomes", "trisetum", "trisemic", "trisemes", "trisects", "triscele", "triremes", "triratna", "triradii", "tripodic", "tridacna", "trickful", "trickers", "trichord", "trichoma", "trichoid", "trichode", "trichite", "trichion", "trichina", "triceria", "trickily", "trickish", "tricklet", "trictrac", "tricouni", "tricotee", "tricosyl", "tricorns", "tricorne", "triconch", "tricolon", "tricolic", "triclads", "tributer", "tribular", "triander", "triamino", "triamine", "triamide", "triality", "trialism", "trialate", "triadist", "triadism", "triadics", "triapsal", "triarchy", "triareal", "tribrach", "tribelet", "tribasic", "tribadic", "tribades", "triazoic", "triazins", "triazane", "triaster", "triarian", "triacids", "trimtram", "trilloes", "trilliin", "trilleto", "trillers", "trillado", "trilemma", "trilbies", "trikeria", "trihoral", "trihedra", "trilobal", "trilobed", "trilogic", "trimotor", "trimorph", "trimoric", "trimodal", "trimness", "trimmest", "trimeter", "trimesyl", "trimesic", "trimacer", "trihalid", "trigynia", "trifilar", "triethyl", "triequal", "trientes", "triental", "triennia", "triduums", "tridents", "tridecyl", "triddler", "triflers", "triforia", "trifuran", "trigraph", "trigonum", "trigonon", "trigonid", "trigonic", "trigonia", "trigness", "triglyph", "trigging", "triggest", "tridaily", "triggered", "triangles", "trimester", "tribunals", "tributary", "trimmings", "trigraphs", "trickster", "triennial", "trivially", "tricyclic", "trillions", "triumphed", "trickling", "tricycles", "triumphal", "tribesmen", "trifolium", "triticale", "tricuspid", "tribology", "trivalent", "trimethyl", "tristania", "tribalism", "trilobite", "tricolour", "triplexes", "trilinear", "tribesman", "trickiest", "triennium", "trilogies", "trichloro", "tritiated", "tristesse", "trisodium", "triazines", "triquetra", "trinomial", "tributing", "trichomes", "triazoles", "tripitaka", "trimarans", "tribolium", "trichuris", "triphasic", "triclinic", "tricyrtis", "tripeshop", "triparted", "trioxides", "tripelike", "triperies", "triozonid", "tripewife", "triploidy", "triploids", "triplites", "trisilane", "triorchis", "triplegia", "triplasic", "triplaris", "triplanes", "triphenyl", "triphasia", "triphaser", "triplopia", "triosteum", "trinketer", "trinketed", "trinitrin", "trimotors", "trindling", "trinitrid", "trinities", "trinidado", "trineural", "trinerved", "trinchera", "trination", "trinalize", "trinality", "trinketry", "trinodine", "trinovant", "triopidae", "trimetric", "trionymal", "triolefin", "trioleate", "triolcous", "trioicous", "trioecism", "trimorphs", "triocular", "trioctile", "triobolon", "trinunity", "trimstone", "triweekly", "triticums", "triticoid", "triticism", "triticeum", "trithings", "tritheite", "tritheist", "tritheism", "triteness", "triteleia", "tritanope", "tritactic", "trisulfid", "tristichs", "tristezas", "tritocone", "tritomite", "triverbal", "trivantly", "trivalves", "triumviry", "triumvirs", "triumviri", "triumpher", "triturium", "triturate", "tritoxide", "tritorium", "tritopine", "tritonous", "tritonoid", "tritoness", "trisquare", "trisporic", "tripudium", "tripudist", "tripudial", "triptyque", "triptychs", "triptycas", "triptanes", "tripsacum", "trippings", "tripotage", "tripolite", "tripoline", "tripodies", "tripodian", "tripodial", "tripylaea", "tripylean", "trisonant", "trisomies", "trisomics", "trismuses", "triskelia", "triskeles", "trisetose", "triserial", "trisector", "trisected", "trisceles", "trisagion", "triregnum", "triradius", "triradial", "tripmadam", "trimeters", "tricrural", "trichroic", "trichosis", "trichomic", "trichogen", "trichitis", "trichitic", "trichites", "trichions", "trichinas", "trichinal", "trichinae", "trichilia", "tricerium", "tricerion", "tricepses", "trichrome", "tricinium", "tricrotic", "tricresol", "tricotine", "tricosane", "tricornes", "tricolors", "triclinia", "triactine", "tricksome", "tricksily", "tricksier", "trickment", "tricklike", "tricklier", "trickless", "tricephal", "tricenary", "triazolic", "triatomic", "triarctic", "triannual", "triangula", "triangler", "triangled", "triandria", "triamorph", "trialogue", "triagonal", "triaenose", "triadisms", "triadical", "tribalist", "tribarred", "tricaudal", "tricarbon", "tricalcic", "tributist", "tribunate", "tribunary", "tribuloid", "tribulate", "tribually", "tribromid", "tribrachs", "tribonema", "tribeship", "tribelike", "tribeless", "triadenum", "trimerous", "trihydrol", "trihydric", "trihybrid", "trihourly", "trihedron", "trihedral", "trihalide", "trigynous", "trigynian", "trigonous", "trigonoid", "trigonite", "triglyphs", "triglidae", "trigintal", "trijugate", "trijugous", "trimerite", "trimeride", "trimellic", "trilogist", "trilobita", "trilobate", "trilliums", "trillibub", "trilletto", "trillando", "trilithon", "trilithic", "trilaurin", "triketone", "trikerion", "trigemini", "trigatron", "triennias", "trieennia", "triedness", "triecious", "tridymite", "tridrachm", "tridermic", "tridental", "tridecoic", "tridecene", "tridecane", "tridactyl", "tricycler", "tricycled", "tricuspal", "trierarch", "trierucin", "trigamous", "trigamist", "trifurcal", "trifornia", "triformin", "triformed", "triforium", "triforial", "trifocals", "trifloral", "triflings", "trifledom", "triferous", "trifacial", "trieteric", "trictracs", "triangular", "triggering", "triumphant", "tripartite", "trigeminal", "trilateral", "triplicate", "trilingual", "trimesters", "tricksters", "triviality", "trihydrate", "trivialize", "triumphing", "trinocular", "tricolored", "tridentine", "trinitrate", "triangulum", "tripeptide", "tricalcium", "trigonella", "triggerman", "triskelion", "tricholoma", "trihydroxy", "triborough", "trichechus", "trillionth", "trifoliate", "tripsomely", "triplefold", "tripterous", "trippingly", "triphysite", "tripinnate", "triplasian", "tripleback", "tripolitan", "triplicist", "tripleness", "tripliform", "triplexity", "triploidic", "triplewise", "triplumbic", "tripletree", "tripletail", "tripodical", "tripointed", "triplicity", "triphylite", "triodontes", "trinucleus", "trinacrian", "trinoctile", "trinoctial", "trinketing", "trinkermen", "trinkerman", "trinitride", "trinervate", "trimscript", "trimotored", "trimorphic", "trioecious", "triolefine", "triphyline", "triphthong", "triphibian", "triphammer", "tripewoman", "tripestone", "tripennate", "tripaschal", "triozonide", "trioxazine", "triovulate", "triorchism", "trionychid", "trimonthly", "trivirgate", "triturated", "triturable", "trittichan", "tritozooid", "tritonymph", "tritonidae", "tritoconid", "triticeous", "tritically", "trithrinax", "trithionic", "triterpene", "triternate", "tritanopic", "tritanopia", "triturates", "triturator", "trivialist", "trivialism", "trivialise", "trivetwise", "triverbial", "trivariant", "trivalerin", "trivalency", "trivalence", "triunities", "triungulin", "triumviral", "trivoltine", "triumfetta", "tritylodon", "tritangent", "trisylabic", "triseptate", "trisensory", "trisectrix", "trisection", "trisecting", "triradiate", "triquinoyl", "triquinate", "triquetrum", "triquetric", "triquetral", "tripylaean", "tripunctal", "tripudiate", "tripudiary", "triseriate", "trisilicic", "trisulphid", "trisulfone", "trisulfide", "trisulfate", "trisulcate", "tristylous", "tristichic", "tristfully", "tristeness", "tristearin", "trisporous", "trispinose", "trispaston", "trismegist", "trisinuate", "tripudiant", "triconodon", "tricipital", "trichromic", "trichromat", "trichroism", "trichotomy", "trichopter", "trichopore", "trichoplax", "trichology", "trichogyne", "trichocyst", "trichlorid", "trichiurus", "trichiurid", "trickeries", "trickiness", "tricolette", "tricoccous", "tricoccose", "triclinium", "triclinial", "triclinate", "tricladida", "tricktrack", "tricksiest", "tricksical", "trickproof", "trickliest", "trickishly", "trickingly", "trichinous", "trichinoid", "tribasilar", "triaxonian", "triarthrus", "triarchies", "triarchate", "triapsidal", "trianthous", "triangulid", "triandrous", "triandrian", "triamylose", "triactinal", "triaconter", "triacontad", "tribesfolk", "triblastic", "trichinize", "trichinise", "trichiasis", "trichevron", "trichauxis", "tricentral", "tricennial", "tricaudate", "tricarpous", "tributyrin", "tributable", "tribromide", "tribrachic", "tribometer", "triacetate", "tricornute", "trimnesses", "trilineate", "trilaminar", "trilabiate", "trihydride", "trihemimer", "trihemeral", "trihedrons", "trigraphic", "trigrammic", "trigonitis", "trigonally", "trignesses", "triglyphic", "triglyphed", "trilinguar", "triliteral", "trimmingly", "trimethoxy", "trimestral", "trimesitic", "trimesinic", "trimensual", "trimembral", "trimacular", "triluminar", "trilogical", "trilocular", "trilobitic", "trilobated", "trillachan", "triglyphal", "triglochin", "trielaidin", "tridiurnal", "tridepside", "tridentate", "tridecylic", "tricyclist", "tricycling", "tricyclene", "tricyanide", "tricussate", "tricurvate", "tricrotous", "tricrotism", "tricosylic", "trienniums", "trientalis", "triglochid", "trigesimal", "trigeneric", "trigeminus", "trifurcate", "triformous", "triformity", "triflorous", "triflorate", "triflingly", "trifarious", "trifanious", "trieterics", "trierarchy", "tricostate", "tributaries", "tribulation", "triumvirate", "trinitarian", "triceratops", "tridiagonal", "trichomonas", "trichoderma", "trichoptera", "trinidadian", "triggerfish", "trichloride", "triangulate", "trichinella", "trifluralin", "trichinosis", "trifluoride", "tribunitian", "tristimulus", "trinovantes", "triphyletic", "triphyllous", "tripinnated", "trinorantum", "trinopticon", "trinucleate", "tripemonger", "tripersonal", "tripartient", "tripartible", "tripartedly", "tripalmitin", "tripetaloid", "trionychoid", "tripetalous", "triphibious", "triodontoid", "triplicated", "trinomially", "trinobantes", "trimetallic", "trimetalism", "trimestrial", "trimercuric", "trimellitic", "trimargarin", "trimaculate", "triluminous", "trilophodon", "triloculate", "trimetrical", "trimetrogon", "trimodality", "trinklement", "trinketries", "trinitytide", "trinityhood", "trinational", "trimyristin", "trimuscular", "trimscripts", "trimorphous", "trimorphism", "trilobation", "triweeklies", "tritogeneia", "triticality", "trithionate", "tritheistic", "tritemorion", "tritanoptic", "tritanopsia", "tritagonist", "trisyllable", "trisyllabic", "tritonality", "triturating", "trituration", "trivialness", "trivialised", "trivalvular", "triuridales", "triumphwise", "triumphator", "triumphancy", "triumphance", "triturature", "triturators", "trisulphone", "trisulphide", "trisulphate", "trisections", "trisceptral", "triradiuses", "triradiated", "triradially", "triquetrous", "tripyrenous", "tripylarian", "tripunctate", "triploidite", "trisepalous", "triserially", "triseriatim", "trisulfoxid", "trisulcated", "tristiloquy", "tristichous", "tristearate", "trispermous", "trisotropis", "trisinuated", "trisilicate", "trisilicane", "triplicates", "trillionths", "trichophyte", "trichinotic", "trichinoses", "trichinosed", "trichinized", "trichinised", "trichechine", "tricephalus", "tricephalic", "tricenarium", "tricenaries", "trichiuroid", "trichlorfon", "trichoblast", "trichophore", "trichopathy", "trichonotid", "trichonosus", "trichonosis", "trichomonal", "trichomonad", "trichomanes", "tricholaena", "trichogynic", "tricellular", "tricarinate", "tribadistic", "triaxiality", "triarcuated", "triantelope", "triannulate", "trianguloid", "triammonium", "triadically", "triacontane", "triachenium", "tribasicity", "tribeswoman", "tribeswomen", "tricapsular", "tributorian", "tributarily", "tribunitive", "tribunitial", "tribunician", "tribunicial", "tribuneship", "tribrachial", "tribologist", "triableness", "trillionize", "triglyceryl", "triggerless", "trigeminous", "trifurcated", "trifoliosis", "trifoliated", "trierarchic", "trierarchal", "triennially", "trieciously", "trigonellin", "trigoneutic", "trigoniidae", "trilliaceae", "trilinolate", "trilineated", "trilaminate", "trilamellar", "trijunction", "trihydrated", "trihemiobol", "trigonotype", "trigonodont", "tridynamous", "tridominium", "tricliniary", "triclclinia", "trickstress", "tricksiness", "tricklingly", "tricircular", "trichronous", "trichromate", "trichotomic", "trichostema", "tricolumnar", "tricompound", "triconodont", "tridigitate", "tridiapason", "tridentlike", "tridentated", "tridecylene", "tridacnidae", "tricuspidal", "tricosanone", "tricorporal", "tricornered", "trichorrhea", "trigonometry", "triphosphate", "tribulations", "triglyceride", "triumphantly", "triangulated", "trivialities", "tribological", "trichophyton", "trivializing", "trichogramma", "triangularis", "trinitarians", "tribespeople", "trimolecular", "trimyristate", "trinitration", "trigrammatic", "trinomialism", "trinomialist", "trinomiality", "triodontidae", "trioeciously", "trimaculated", "tripaleolate", "tripalmitate", "tripartitely", "trigonometer", "tripartition", "trimethylene", "trimetallism", "trihemimeris", "trilarcenous", "trilaterally", "trihemimeral", "trilingually", "trilinoleate", "trilinolenin", "triliterally", "trilliaceous", "trillionaire", "trilophodont", "triguttulate", "trimargarate", "trimastigate", "trimeresurus", "trimesitinic", "trionychidae", "triphthongal", "trisulphonic", "trisulphoxid", "trisyllabism", "trisyllabity", "triternately", "triterpenoid", "tritheocracy", "trithionates", "triticalness", "tritonymphal", "tritopatores", "trituberculy", "triweekliess", "triumvirates", "triumvirship", "triunitarian", "triuridaceae", "trisulfoxide", "tristisonous", "tripinnately", "triplicately", "triplicating", "triplication", "triplicative", "triplicature", "triplicities", "triplinerved", "tripotassium", "trippingness", "tripudiation", "triradiately", "triradiation", "trismegistic", "tristachyous", "tristfulness", "tristigmatic", "trivialising", "trigoniacean", "triacetamide", "trichinizing", "trichinopoli", "trichinopoly", "trichiuridae", "trichobezoar", "trichoclasia", "trichoclasis", "trichocystic", "trichogenous", "trichogynial", "trichologist", "trichomatose", "trichomatous", "trichopathic", "trichophobia", "trichophoric", "trichophytia", "trichinising", "trichiniasis", "trichechidae", "triadelphous", "triamorphous", "triangleways", "trianglewise", "trianglework", "triangularly", "triangulates", "triangulator", "triatomicity", "tribophysics", "tribracteate", "tribunitiary", "tricarbimide", "tricarinated", "tricenarious", "tricentenary", "tricephalous", "trichophytic", "trichopteran", "tridactylous", "tridentinian", "tridiametral", "trienniality", "trierarchies", "trifasciated", "trifistulary", "triflingness", "trifluouride", "trifoliolate", "trifoveolate", "trifurcating", "trifurcation", "triglandular", "triglyphical", "trigonelline", "trigoneutism", "tricuspidate", "tricoryphean", "tricorporate", "trichopteron", "trichosporum", "trichostasis", "trichotomies", "trichotomism", "trichotomist", "trichotomize", "trichotomous", "trichromatic", "trichuriases", "trichuriasis", "trickishness", "trickstering", "tricliniarch", "triconodonta", "triconodonty", "tricophorous", "trigoniaceae", "triglycerides", "triangulation", "trigonometric", "triamcinolone", "trinucleotide", "triethylamine", "triangulating", "tricarboxylic", "trichodesmium", "tricentennial", "trilateration", "trinitroxylol", "trimethadione", "triodontoidea", "triodontoidei", "trioperculate", "triorthogonal", "trimucronatus", "trihypostatic", "trimerization", "triliterality", "trilamellated", "triliteralism", "trilaterality", "trilinolenate", "trilingualism", "tripersonally", "triphenylated", "tripinnatifid", "tristichaceae", "tristigmatose", "trisulphoxide", "trisyllabical", "tritangential", "tritheistical", "tritocerebral", "tritocerebrum", "trisplanchnic", "trisaccharose", "trisaccharide", "triplications", "triplicostate", "triploblastic", "triplocaulous", "triquadrantal", "triquetrously", "trirhomboidal", "triricinolein", "tritubercular", "trigrammatism", "trigonometria", "tricarpellate", "triceratopses", "trichatrophia", "trichechodont", "trichinoscope", "trichinoscopy", "trichocarpous", "trichoglossia", "tricarpellary", "tributariness", "triangulately", "triarticulate", "triatomically", "tribesmanship", "triboelectric", "tribonemaceae", "tribromacetic", "tribromphenol", "trichological", "trichomaphyte", "trichromatist", "triconodontid", "tricuspidated", "triflagellate", "triggerfishes", "trigintennial", "trigoniaceous", "trigonocerous", "trichromatism", "trichothallic", "trichomatosis", "trichomonadal", "trichomycosis", "trichopterous", "trichorrhexic", "trichorrhexis", "trichosanthes", "trichoschisis", "triangularity", "triangulations", "trichomoniasis", "trivialization", "trimethylamine", "tripelennamine", "trionychoidean", "trinitroxylene", "trinitrotoluol", "trinitrophenol", "trinitrocresol", "trinitarianism", "triliteralness", "trilateralness", "triiodomethane", "trihemiobolion", "trigonometries", "tripersonalism", "tripersonalist", "trivialisation", "triunsaturated", "triunification", "trituberculism", "trituberculata", "tritriacontane", "tritencephalon", "trisubstituted", "trisoctahedron", "trisoctahedral", "trirectangular", "tripinnatisect", "triphenylamine", "tripersonality", "trigonocephaly", "tridimensioned", "trichobranchia", "trichobacteria", "trichinophobia", "trichinization", "trichinisation", "trichiniferous", "tricentennials", "tricentenarian", "tricarballylic", "tribromphenate", "tribromophenol", "tribracteolate", "triantaphyllos", "trichocephalus", "trichodontidae", "trichoglossine", "tridimensional", "tridentiferous", "tridecilateral", "triconsonantal", "triconodontoid", "triclinohedric", "trichotomously", "trichosporange", "trichoschistic", "trichopterygid", "trichophytosis", "trichophyllous", "trichomonacide", "triacetonamine", "trichloroethane", "triethanolamine", "trifluoperazine", "trichloroacetic", "trinitrotoluene", "trinitromethane", "trinitrobenzene", "trinitroaniline", "trimethylacetic", "triacontaeterid", "triodontophorus", "trioxymethylene", "triphenylmethyl", "trirhombohedral", "tristetrahedron", "trisubstitution", "trisyllabically", "trithioaldehyde", "trigonometrical", "trigonocephalus", "tribromoethanol", "trichlormethane", "trichloromethyl", "trichoglossidae", "trichoglossinae", "trichomonacidal", "trichomonadidae", "trichoschistism", "trichostrongyle", "trichromatopsia", "tricotyledonous", "triethylstibine", "trigonocephalic", "trithiocarbonic", "triiodothyronine", "trichotillomania", "trimethylbenzene", "trimethylglycine", "trimethylmethane", "trimethylstibine", "trinitrocarbolic", "trinitroglycerin", "trinitroresorcin", "triphenylmethane", "triplocaulescent", "triskaidekaphobe", "tritetartemorion", "triboelectricity", "trigonometrician", "trigonocephalous", "tribofluorescent", "triboluminescent", "trichlorethylene", "trichloromethane", "trichobranchiate", "trichopterygidae", "trichosporangial", "trichosporangium", "trichostrongylid", "trichostrongylus", "tridimensionally", "trithiocarbonate", "trichloroethylene", "triakisoctahedral", "tridimensionality", "trigonometrically", "trinitrocellulose", "trionychoideachid", "triphenylcarbinol", "triplochitonaceae", "trisacramentarian", "triskaidekaphobes", "triconsonantalism", "trichopathophobia", "trichogrammatidae", "triakisoctahedrid", "triakisoctahedron", "tribofluorescence", "triboluminescence", "trichlorethylenes", "trichloromethanes", "trichocephaliasis", "trichoepithelioma", "triskaidekaphobia", "triphenylphosphine", "triakisicosahedral", "triakisicosahedron", "triakistetrahedral", "triakistetrahedron", "triangulopyramidal"} + + // The whole list would produce an insanely big SVG that chromium browsers can't handle (500% zoom is not enough), so focus on words with prefix "trip" + words := make([]string, 0, 1000) + words = append(words, "") + for _, w := range allWords { + if strings.HasPrefix(w, "trip") { + words = append(words, w) + } + } + + prefixMap := map[string]int{} + parentMap := make([]int, len(words)) + for idx, w := range words { + prefixMap[w] = idx + } + for idx, w := range words { + if len(w) <= len(rootWord) { + continue + } + prefixLen := len(w) - 1 + for { + prefixIdx, ok := prefixMap[w[:prefixLen]] + if ok { + parentMap[idx] = prefixIdx + break + } + prefixLen-- + } + } + nodeDefs := make([]NodeDef, len(words)) + for idx, w := range words { + prefix := words[parentMap[idx]] + nodeDefs[idx] = NodeDef{int16(idx), w[len(prefix):], EdgeDef{int16(parentMap[idx]), ""}, []EdgeDef{}, "", 0, false} + } + // Don't even try optimized, it will ask for fact(51) + svg, _, err := DrawUnoptimized(nodeDefs, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions(), DefaultEdgeOptions(), "", "", DefaultPalette()) + assert.Equal(t, nil, err) + fmt.Printf("%s\n", svg) +} diff --git a/pkg/capigraph/test_vizzes.go b/pkg/capigraph/test_vizzes.go new file mode 100644 index 00000000..3916cd65 --- /dev/null +++ b/pkg/capigraph/test_vizzes.go @@ -0,0 +1,394 @@ +package capigraph + +// 0: 1 3 +// - |/ +// 1: 2 +var testNodeDefsBasic = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{{3, "from 3"}}, "", 0, false}, + {3, "3", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 3 +// - | | +// 1: 2 4 +var testNodeDefsTrivialParallel = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | \ +// 1: 2 6 3 +// - | / \| +// 2: 4 5 +var testNodeDefsOneEnclosedOneLevel = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{2, ""}, []EdgeDef{{6, ""}}, "", 0, false}, + {5, "5", EdgeDef{3, ""}, []EdgeDef{{6, ""}}, "", 0, false}, + {6, "6", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | \ +// 1: 2 5 +// - | | +// 2: 3 8 6 +// - | / \ | +// 3: 4 7 +var testNodeDefsOneEnclosedTwoLevels = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{{8, ""}}, "", 0, false}, + {5, "5", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{5, ""}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{6, ""}, []EdgeDef{{8, ""}}, "", 0, false}, + {8, "8", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | \ +// 1: 2 4 +// - | | +// 2: 3 5 +var testNodeDefsNoIntervals = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 2 3 4 5 6 7 8 9 10 +var testNodeDefsFlat10 = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {8, "8", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {9, "9", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {10, "10", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | \ +// 1: 2 3 +// - | | +// 2: 4 8 5 9 +// - | \| / +// - | / / | / +// - | // | +// 2: 6 7 +var testNodeDefsTwoEnclosedNodeSizeMatters = []NodeDef{ + {0, "", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{4, ""}, []EdgeDef{{8, ""}, {9, ""}}, "", 0, false}, + {7, "7", EdgeDef{5, ""}, []EdgeDef{{8, ""}, {9, ""}}, "", 0, false}, + {8, "8", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {9, "9", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 3 +// - | | +// 1: 2 4 6 +// - | / +// 2: 5 +var testNodeDefsOneSecondary = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{{6, ""}}, "", 0, false}, + {6, "6", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - / | \ +// 1: 2 3 4 6 +// - | // +// 2: 5 +var testNodeDefsDiamond = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{3, ""}, []EdgeDef{{4, ""}, {6, ""}}, "", 0, false}, + {6, "6", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +var testNodeDefs40milPerms = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + + {5, "5", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {8, "8", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {9, "9", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {10, "10", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {11, "11", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {12, "12", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {13, "13", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {14, "14", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {15, "15", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + {16, "16", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + {17, "17", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + {18, "18", EdgeDef{4, ""}, []EdgeDef{{20, ""}}, "", 0, false}, + // {19, "19", EdgeDef{4, ""}, []EdgeDef{{21, ""}}, "", 0, false}, + {19, "19", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + + {20, "20", EdgeDef{}, []EdgeDef{}, "", 0, false}, + // {21, "21", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +var testNodeDefs300bilPerms = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + + {5, "5", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {8, "8", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {9, "9", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {10, "10", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {11, "11", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {12, "12", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {13, "13", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {14, "14", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {15, "15", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {16, "16", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + {17, "17", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + {18, "18", EdgeDef{4, ""}, []EdgeDef{{22, ""}}, "", 0, false}, + {19, "19", EdgeDef{4, ""}, []EdgeDef{{23, ""}}, "", 0, false}, + {20, "20", EdgeDef{4, ""}, []EdgeDef{{24, ""}}, "", 0, false}, + {21, "21", EdgeDef{4, ""}, []EdgeDef{{25, ""}}, "", 0, false}, + + {22, "22", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {23, "23", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {24, "24", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {25, "25", EdgeDef{}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 2 +// - | /| +// 1: 3 | +// - | / +// 2: 4 +var testNodeDefsTwoLevelsFromOneParent = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{1, ""}, []EdgeDef{{2, ""}}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{{2, ""}}, "", 0, false}, +} + +// 0: 1 +// - | \ +// 1: 2 5 +// - | | +// 2: 3 F +// - | \ | +// 3: 4 6 +// - | +// 4: 7 +var testNodeDefsTwoLevelsFromOneParentSameRoot = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{5, ""}, []EdgeDef{{3, ""}}, "", 0, false}, + {7, "7", EdgeDef{6, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | \ +// 1: 2 6 +// - | | +// 2: 3 F +// - | | +// 3: 4 F +// - | \ | +// 4: 5 7 +// - | +// 5: 8 +var testNodeDefsTwoLevelsFromOneParentSameRootTwoFakes = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{6, ""}, []EdgeDef{{4, ""}}, "", 0, false}, + {8, "8", EdgeDef{7, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | +// 1: 2 +// - | +// 2: 3 5 +// - | / | +// 3: 4 6 +// - | +// 4: 7 +var testNodeDefsSubtreeBelowLong = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{{5, ""}}, "", 0, false}, + {5, "5", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{5, ""}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{6, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | +// 1: 2 6 +// - | / | +// 2: 3 / 7 +// - | / | +// 3: 4 8 +// - | / +// 4: 5 +var testNodeDefsOneNotTwoLevelsDown = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{{6, ""}}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{{8, ""}}, "", 0, false}, + {6, "6", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{6, ""}, []EdgeDef{}, "", 0, false}, + {8, "8", EdgeDef{7, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - | +// 1: 2 5 +// - | /| +// 2: 3 / 6 +// - | // +// 3: 4 +var testNodeDefsMultiSecParentPullDown = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{{5, ""}, {6, ""}}, "", 0, false}, + {5, "5", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{5, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 4 +// - | /| +// 1: 2 / 5 +// - |// +// 2: 3 +var testNodeDefsMultiSecParentNoPullDown = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{{4, ""}, {5, ""}}, "", 0, false}, + {4, "4", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{4, ""}, []EdgeDef{}, "", 0, false}, +} + +// 0: 1 +// - \\\\\\ +// 1: 2 3 4 5 6 +var testNodeDefsDuplicateSecLabels = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + + {2, "2", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{}, []EdgeDef{}, "", 0, false}, + + {7, "7", EdgeDef{2, ""}, []EdgeDef{{1, "txt"}}, "", 0, false}, + {8, "8", EdgeDef{3, ""}, []EdgeDef{{1, "txt"}}, "", 0, false}, + {9, "9", EdgeDef{4, ""}, []EdgeDef{{1, "txt"}}, "", 0, false}, + {10, "10", EdgeDef{5, ""}, []EdgeDef{{1, "txt"}}, "", 0, false}, + {11, "11", EdgeDef{6, ""}, []EdgeDef{{1, "txt"}}, "", 0, false}, +} + +// 0: 5 +// - | +// 1: 6 - - +// - | \ +// 2: 7 1 | +// - | | | +// 3: 8 2 | 9 +// - \ | | | +// 4: 3 | 10 +// - | / | +// 5: 4 11 +// - \ | +// 6: 12 +var testNodeDefsLayerLongRoots = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{{8, ""}}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{{6, ""}}, "", 0, false}, + {5, "5", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{5, ""}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{6, ""}, []EdgeDef{}, "", 0, false}, + {8, "8", EdgeDef{7, ""}, []EdgeDef{}, "", 0, false}, + {9, "9", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {10, "10", EdgeDef{9, ""}, []EdgeDef{}, "", 0, false}, + {11, "11", EdgeDef{10, ""}, []EdgeDef{}, "", 0, false}, + {12, "12", EdgeDef{11, ""}, []EdgeDef{{4, ""}}, "", 0, false}, +} + +// 0: 1 +// - | +// 1: 2 ------\ +// - | \ | +// 2: 3 6 | +// - |\ |\// +// 3: 4 5 7 8 +var testNodeDefsPriAndSecInfinitePulldown = []NodeDef{ + {0, "top node", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {1, "1", EdgeDef{}, []EdgeDef{}, "", 0, false}, + {2, "2", EdgeDef{1, ""}, []EdgeDef{}, "", 0, false}, + {3, "3", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {4, "4", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {5, "5", EdgeDef{3, ""}, []EdgeDef{}, "", 0, false}, + {6, "6", EdgeDef{2, ""}, []EdgeDef{}, "", 0, false}, + {7, "7", EdgeDef{6, ""}, []EdgeDef{{2, ""}}, "", 0, false}, + {8, "8", EdgeDef{6, ""}, []EdgeDef{{2, ""}}, "", 0, false}, +} diff --git a/pkg/capigraph/viz_node.go b/pkg/capigraph/viz_node.go new file mode 100644 index 00000000..e6fb9162 --- /dev/null +++ b/pkg/capigraph/viz_node.go @@ -0,0 +1,71 @@ +package capigraph + +import ( + "fmt" + "strings" +) + +type HierarchyType int + +const ( + HierarchyPri HierarchyType = iota + HierarchySec +) + +type VizNode struct { + Def *NodeDef + IncomingVizEdges []VizEdge + RootId int16 + Layer int + TotalW float64 + X float64 // Total X + Y float64 + NodeW float64 + NodeH float64 + PriChildrenAndEnclosedRoots []*VizNode +} + +func (vn *VizNode) String() string { + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("{Id:%d RootId:%d Layer:%d ", vn.Def.Id, vn.RootId, vn.Layer)) + sb.WriteString(fmt.Sprintf("X:%.2f Y:%.2f TotalW:%.2f W:%.2f H:%.2f ", vn.X, vn.Y, vn.TotalW, vn.NodeW, vn.NodeH)) + sb.WriteString("In:[") + for i, e := range vn.IncomingVizEdges { + if i > 0 { + sb.WriteString(" ") + } + ht := "pri" + if e.HierarchyType == HierarchySec { + ht = "sec" + } + sb.WriteString(fmt.Sprintf("{HT:%s SrcId:%d X:%.2f Y:%.2f W:%.2f H:%.2f}", ht, e.Edge.SrcId, e.X, e.Y, e.W, e.H)) + } + sb.WriteString("] ") + sb.WriteString("PriRootOut:[") + for i, c := range vn.PriChildrenAndEnclosedRoots { + if i > 0 { + sb.WriteString(" ") + } + sb.WriteString(fmt.Sprintf("%d", c.Def.Id)) + } + sb.WriteString("]") + sb.WriteString("}") + return sb.String() +} + +func (vn *VizNode) cleanPropertiesSubjectToPermutation() { + // Do not clean up static properties like Def, NodeW, NodeH etc + vn.PriChildrenAndEnclosedRoots = vn.PriChildrenAndEnclosedRoots[:0] // Reset size, not capacity + vn.TotalW = 0.0 + vn.X = 0.0 + vn.Y = 0.0 +} + +type VizEdge struct { + Edge EdgeDef + HierarchyType HierarchyType + X float64 + Y float64 + W float64 + H float64 +} diff --git a/pkg/capigraph/viz_node_hierarchy.go b/pkg/capigraph/viz_node_hierarchy.go new file mode 100644 index 00000000..b8d8870d --- /dev/null +++ b/pkg/capigraph/viz_node_hierarchy.go @@ -0,0 +1,607 @@ +package capigraph + +import ( + "fmt" + "math" + "slices" + "sort" + "strings" + "time" +) + +type RectDimension struct { + W float64 + H float64 +} + +type VizNodeHierarchy struct { + VizNodeMap []VizNode + NodeDefs []NodeDef + PriParentMap []int16 + RootMap []int16 + NodeFo FontOptions + EdgeFo FontOptions + NodeDimensionMap []RectDimension + PriEdgeLabelDimMap []RectDimension + SecEdgeLabelDimMap [][]RectDimension + TotalLayers int + UpperLayerGapMap []float64 +} + +func (vnh *VizNodeHierarchy) String() string { + sb := strings.Builder{} + for i, vn := range vnh.VizNodeMap { + if i == 0 { + continue + } + if i > 1 { + sb.WriteString(", ") + } + sb.WriteString(vn.String()) + } + return sb.String() +} + +func NewVizNodeHierarchy(nodeDefs []NodeDef, nodeFo FontOptions, edgeFo FontOptions) *VizNodeHierarchy { + vnh := VizNodeHierarchy{} + vnh.NodeDefs = nodeDefs + vnh.PriParentMap = buildPriParentMap(nodeDefs) + vnh.RootMap = buildNodeToRootMap(vnh.PriParentMap) + vnh.NodeFo = nodeFo + vnh.EdgeFo = edgeFo + vnh.NodeDimensionMap = make([]RectDimension, len(nodeDefs)) + for i := range len(nodeDefs) - 1 { + nodeDef := nodeDefs[i+1] + vnh.NodeDimensionMap[nodeDef.Id] = getNodeDimensions(&nodeDef, vnh.NodeFo) + } + + vnh.PriEdgeLabelDimMap = make([]RectDimension, len(nodeDefs)) + vnh.SecEdgeLabelDimMap = make([][]RectDimension, len(nodeDefs)) + for _, nodeDef := range nodeDefs { + if nodeDef.PriIn.SrcId != 0 { + w, h := getTextDimensions(nodeDef.PriIn.Text, edgeFo.Typeface, edgeFo.Weight, edgeFo.SizeInPixels, edgeFo.Interval) + vnh.PriEdgeLabelDimMap[nodeDef.Id].W, vnh.PriEdgeLabelDimMap[nodeDef.Id].H = getLabelDimensionsFromTextDimensions(w, h, edgeFo.SizeInPixels*LabelTextDimensionMargin) + } + vnh.SecEdgeLabelDimMap[nodeDef.Id] = make([]RectDimension, len(nodeDef.SecIn)) + for edgeIdx, edgeDef := range nodeDef.SecIn { + w, h := getTextDimensions(edgeDef.Text, edgeFo.Typeface, edgeFo.Weight, edgeFo.SizeInPixels, edgeFo.Interval) + vnh.SecEdgeLabelDimMap[nodeDef.Id][edgeIdx].W, vnh.SecEdgeLabelDimMap[nodeDef.Id][edgeIdx].H = getLabelDimensionsFromTextDimensions(w, h, edgeFo.SizeInPixels*LabelTextDimensionMargin) + } + } + return &vnh +} + +func (vnh *VizNodeHierarchy) insertRootToNearestParent(rootVizNode *VizNode, leftId int16, rightId int16) { + leftParentVisitedMap := make([]*VizNode, len(vnh.VizNodeMap)) + + // Left branch: walk up and harvest ids + leftChildId := leftId + for { + leftParentId := vnh.PriParentMap[sanitizeFakeNodeId(leftChildId)] + if leftParentId == MissingNodeId { + break + } + leftParentVisitedMap[leftParentId] = &(vnh.VizNodeMap[leftParentId]) + leftChildId = leftParentId + } + + // Right branch: go up, identify first common parent, insert + var rightParentId int16 + rightChildId := sanitizeFakeNodeId(rightId) + for { + rightParentId = vnh.PriParentMap[rightChildId] + commonParentVizNode := leftParentVisitedMap[rightParentId] + if commonParentVizNode != nil { + // Now rightChildId and commonParentVizNode contain the place to insert + for childIdx, childVizNode := range commonParentVizNode.PriChildrenAndEnclosedRoots { + if childVizNode.Def.Id == rightChildId { + commonParentVizNode.PriChildrenAndEnclosedRoots = slices.Insert(commonParentVizNode.PriChildrenAndEnclosedRoots, childIdx, rootVizNode) + return + } + } + panic(fmt.Sprintf("insertRootToNearestParent for root id %d cannot find parent for left %d and right %d", rootVizNode.Def.Id, leftId, rightId)) + } + rightChildId = rightParentId + } +} + +func (vnh *VizNodeHierarchy) insertRoot(rootVizNode *VizNode, perm []int16, idx int) { + thisRootId := perm[idx] + // Start at the left neighbour, move left + i := idx - 1 + for i >= 0 { + leftRootId := vnh.RootMap[sanitizeFakeNodeId(perm[i])] + if leftRootId != thisRootId { + // Left neighbour belongs to another AND the closest root subtree, now find the nearest common parent for enclosure. + // Start with the nearest right neighbour and move right. + j := idx + 1 + for j < len(perm) { + if leftRootId == vnh.RootMap[sanitizeFakeNodeId(perm[j])] { + // Same root as the left neighbour. Now: + // - go up and find the nearst common parent. + // - insert rootVizNode between first-generation children of the nearst common parent + vnh.insertRootToNearestParent(rootVizNode, perm[i], perm[j]) + return + } + j++ + } + } + i-- + } + + // No enclose, resort to adding it to the top ubernode + topItem := &(vnh.VizNodeMap[0]) + topItem.PriChildrenAndEnclosedRoots = append(topItem.PriChildrenAndEnclosedRoots, rootVizNode) +} + +func (vnh *VizNodeHierarchy) buildNewRootSubtreeHierarchy(mx LayerMx) { + vnh.VizNodeMap = make([]VizNode, len(vnh.NodeDefs)) + topItem := &(vnh.VizNodeMap[0]) + topItem.Layer = -1 + topItem.PriChildrenAndEnclosedRoots = make([]*VizNode, 0, MaxLayerLen) + + vnh.TotalLayers = 0 + // Initialize static (non-hierarchy-related) properties + for layer, row := range mx { + if layer+1 > vnh.TotalLayers { + vnh.TotalLayers = layer + 1 + } + for _, nodeId := range row { + // Static properties: remain the same regardless of the mx + vizNode := &(vnh.VizNodeMap[sanitizeFakeNodeId(nodeId)]) + vizNode.Def = &(vnh.NodeDefs[sanitizeFakeNodeId(nodeId)]) + vizNode.RootId = vnh.RootMap[sanitizeFakeNodeId(nodeId)] + if nodeId > FakeNodeBase { + // For now, set it to -1, later iteratin will eventually set it to proper layer (where the actual child node resides) + // This is not crucial, but may help when troubleshooting + vizNode.Layer = -1 + } else { + // This is the real node - the end of fake seq + vizNode.Layer = layer + } + r := vnh.NodeDimensionMap[vizNode.Def.Id] + vizNode.NodeW = r.W + vizNode.NodeH = r.H + incomingEdgesLen := len(vizNode.Def.SecIn) + if vizNode.Def.PriIn.SrcId != 0 { + incomingEdgesLen++ + } + vizNode.IncomingVizEdges = make([]VizEdge, incomingEdgesLen) // Just pre-allocate, it will be populated by PopulateEdgeLabelDimensions + + // Properties to change from mx to mx + vizNode.PriChildrenAndEnclosedRoots = make([]*VizNode, 0, MaxLayerLen) // No need to fill at the moment, just pre-allocate + } + } +} + +func (vnh *VizNodeHierarchy) reuseRootSubtreeHierarchy(mx LayerMx) { + vnh.VizNodeMap[0].cleanPropertiesSubjectToPermutation() + vnh.VizNodeMap[0].Layer = -1 + + // Re-init non-static properties and add pri children to PriChildrenAndEnclosedRoots + for layer, row := range mx { + for _, nodeId := range row { + vizNode := &vnh.VizNodeMap[sanitizeFakeNodeId(nodeId)] + vizNode.cleanPropertiesSubjectToPermutation() + // Do not handle root items yet - handling them requires enclosing tree in place. + // For non-roots - add it to some node on some previous layer in the order they appear on this layer, honoring the perm + rootId := vnh.RootMap[sanitizeFakeNodeId(nodeId)] + if rootId != nodeId { + // Add only the last node of the fake sequence, so there is only one copy of it among the children + if vizNode.Layer == layer { + parentVizNode := &vnh.VizNodeMap[vnh.PriParentMap[sanitizeFakeNodeId(nodeId)]] + parentVizNode.PriChildrenAndEnclosedRoots = append(parentVizNode.PriChildrenAndEnclosedRoots, vizNode) + } + } + } + } + + // Add roots to PriChildrenAndEnclosedRoots (now i's ok to do that) + for _, row := range mx { + for j, nodeId := range row { + if nodeId < FakeNodeBase { + rootId := vnh.RootMap[nodeId] + if rootId == nodeId { + vn := &(vnh.VizNodeMap[nodeId]) + vnh.insertRoot(vn, row, j) + } + } + } + } +} + +const NodeTextDimensionMargin float64 = 1.0 +const NodeTextIconInterval float64 = 1.0 +const LabelTextDimensionMargin float64 = 0.5 + +func getNodeDimensions(nodeDef *NodeDef, fo FontOptions) RectDimension { + w, h := getTextDimensions(nodeDef.Text, fo.Typeface, fo.Weight, fo.SizeInPixels, fo.Interval) + w += float64(fo.SizeInPixels) * NodeTextDimensionMargin * 2 // left+right + if nodeDef.IconId != "" { + // Add space for the HxH icon plus font-size*some coefficient + w += h + fo.SizeInPixels*NodeTextIconInterval + } + h += float64(fo.SizeInPixels) * NodeTextDimensionMargin * 2 // top+bottom + return RectDimension{w, h} +} + +const ( + SecEdgeLabelGapFromSourceInLines float64 = 5.0 + PriEdgeLabelGapFromDestinatioInLines float64 = 2.0 + gapBetweenSecAndPrimeEdgeLabelsInPixels float64 = 10.0 + NodeHorizontalGapInPixels float64 = 20.0 + SecEdgeOffsetX float64 = 10 +) + +func (vnh *VizNodeHierarchy) populateNodeTotalWidthRecursive(vizNode *VizNode) { + // Recursively visit children and add their TotalW to this TotalW + for i, childItem := range vizNode.PriChildrenAndEnclosedRoots { + vnh.populateNodeTotalWidthRecursive(childItem) + if i != 0 { + vizNode.TotalW += NodeHorizontalGapInPixels + } + vizNode.TotalW += childItem.TotalW + } + + // If this node has really wide text, it may be even wider than + // children subtree. Pay attention to this case. + if vizNode.Def != nil { + if vizNode.TotalW < vizNode.NodeW { + vizNode.TotalW = vizNode.NodeW + } + } +} + +func (vnh *VizNodeHierarchy) PopulateNodeTotalWidth() { + vnh.populateNodeTotalWidthRecursive(&vnh.VizNodeMap[0]) +} +func populateNodeXCoordRecursive(vizNode *VizNode) { + // Decide where to start drawing child items: their cumulative width may be well smaller than parent's + cumulativeChildrenAndEnclosedRootsWidth := 0.0 + for j, childItem := range vizNode.PriChildrenAndEnclosedRoots { + cumulativeChildrenAndEnclosedRootsWidth += childItem.TotalW + if j != len(vizNode.PriChildrenAndEnclosedRoots)-1 { + cumulativeChildrenAndEnclosedRootsWidth += NodeHorizontalGapInPixels + } + } + curX := vizNode.X + (vizNode.TotalW-cumulativeChildrenAndEnclosedRootsWidth)/2 + for _, childItem := range vizNode.PriChildrenAndEnclosedRoots { + childItem.X = curX + populateNodeXCoordRecursive(childItem) + curX += childItem.TotalW + NodeHorizontalGapInPixels + } +} + +func (vnh *VizNodeHierarchy) PopulateNodesXCoords() { + vnh.VizNodeMap[0].X = 0 + populateNodeXCoordRecursive(&vnh.VizNodeMap[0]) +} + +func (vnh *VizNodeHierarchy) CalculateTotalHorizontalShift() float64 { + sum := 0.0 + for i := range len(vnh.VizNodeMap) { + tgtVizNode := &vnh.VizNodeMap[i] + if tgtVizNode.Def != nil && len(tgtVizNode.Def.SecIn) > 0 { + for _, edge := range tgtVizNode.Def.SecIn { + srcVizNode := vnh.VizNodeMap[edge.SrcId] + startX := srcVizNode.X + srcVizNode.TotalW/2.0 - srcVizNode.NodeW/2.0 + endX := tgtVizNode.X + tgtVizNode.TotalW/2.0 - tgtVizNode.NodeW/2.0 + sum += math.Abs(endX - startX) + } + } + } + return sum +} + +// Merely copies pre-calculated edge label dimensions to the hierarchy vizitems +func (vnh *VizNodeHierarchy) PopulateEdgeLabelDimensions() { + for i := range len(vnh.VizNodeMap) - 1 { + dstVizNode := &(vnh.VizNodeMap[i+1]) + + // Pri edge + incomingEdgeIdx := 0 + if dstVizNode.Def.PriIn.SrcId != 0 { + labelRectDim := vnh.PriEdgeLabelDimMap[dstVizNode.Def.Id] + dstVizNode.IncomingVizEdges[incomingEdgeIdx] = VizEdge{dstVizNode.Def.PriIn, HierarchyPri, 0.0, 0.0, labelRectDim.W, labelRectDim.H} + incomingEdgeIdx++ + } + + // Sec edges + secLabelRectDims := vnh.SecEdgeLabelDimMap[dstVizNode.Def.Id] + for edgeIdx, edge := range dstVizNode.Def.SecIn { + dstVizNode.IncomingVizEdges[incomingEdgeIdx] = VizEdge{edge, HierarchySec, 0.0, 0.0, 0.0, 0.0} + if secLabelRectDims[edgeIdx].W > 0.0 { + dstVizNode.IncomingVizEdges[incomingEdgeIdx].W = secLabelRectDims[edgeIdx].W + dstVizNode.IncomingVizEdges[incomingEdgeIdx].H = secLabelRectDims[edgeIdx].H + } + incomingEdgeIdx++ + } + } +} + +func (vnh *VizNodeHierarchy) PopulateUpperLayerGapMap(edgeFontSizeInPixels float64) { + minLayerGap := math.Max(vnh.VizNodeMap[0].TotalW/20.0, vnh.NodeFo.SizeInPixels*3.0) // Purely empiric + maxPriEdgeLabelHightMap := slices.Repeat([]float64{-1.0}, vnh.TotalLayers) + maxSecEdgeLabelHightMap := slices.Repeat([]float64{-1.0}, vnh.TotalLayers) + for i := range len(vnh.VizNodeMap) - 1 { + hi := &vnh.VizNodeMap[i+1] + for _, edge := range hi.IncomingVizEdges { + if edge.HierarchyType == HierarchyPri { + prevMaxEdgeLabelHeight := maxPriEdgeLabelHightMap[hi.Layer] + if prevMaxEdgeLabelHeight == -1 || prevMaxEdgeLabelHeight < edge.H { + maxPriEdgeLabelHightMap[hi.Layer] = edge.H + } + } else if edge.HierarchyType == HierarchySec { + // Make sure it's for the correspondent layer, + // otherwise it's not gonna work for cases when an edge goes up more than one level + layer := vnh.VizNodeMap[edge.Edge.SrcId].Layer + 1 + prevMaxEdgeLabelHeight := maxSecEdgeLabelHightMap[layer] + if prevMaxEdgeLabelHeight == -1 || prevMaxEdgeLabelHeight < edge.H { + maxSecEdgeLabelHightMap[layer] = edge.H + } + } else { + panic(fmt.Sprintf("PopulateUpperLayerGapMap: unknown hierarchy type %d", edge.HierarchyType)) + } + } + + // Make sure there are no empty map elements for each layer + if maxPriEdgeLabelHightMap[hi.Layer] == -1 { + maxPriEdgeLabelHightMap[hi.Layer] = 0 + } + if maxSecEdgeLabelHightMap[hi.Layer] == -1 { + maxSecEdgeLabelHightMap[hi.Layer] = 0 + } + } + + vnh.UpperLayerGapMap = make([]float64, vnh.TotalLayers) + for layer, maxPriEdgeLabelHeight := range maxPriEdgeLabelHightMap { + maxSecEdgeLabelHeight := maxSecEdgeLabelHightMap[layer] + if maxSecEdgeLabelHeight > 0 && maxPriEdgeLabelHeight > 0 { + vnh.UpperLayerGapMap[layer] = edgeFontSizeInPixels*SecEdgeLabelGapFromSourceInLines + maxSecEdgeLabelHeight + gapBetweenSecAndPrimeEdgeLabelsInPixels + maxPriEdgeLabelHeight + edgeFontSizeInPixels*PriEdgeLabelGapFromDestinatioInLines + } else if maxSecEdgeLabelHeight > 0 { + // Only sec labels + vnh.UpperLayerGapMap[layer] = edgeFontSizeInPixels*SecEdgeLabelGapFromSourceInLines + maxSecEdgeLabelHeight + edgeFontSizeInPixels*SecEdgeLabelGapFromSourceInLines + } else if maxPriEdgeLabelHeight > 0 { + // Only pri labels here + vnh.UpperLayerGapMap[layer] = edgeFontSizeInPixels*PriEdgeLabelGapFromDestinatioInLines + maxPriEdgeLabelHeight + edgeFontSizeInPixels*PriEdgeLabelGapFromDestinatioInLines + } else { + vnh.UpperLayerGapMap[layer] = 0 + } + if vnh.UpperLayerGapMap[layer] < minLayerGap { + vnh.UpperLayerGapMap[layer] = minLayerGap + } + } +} + +func populateLayerHeightsRecursive(vizNode *VizNode, layerHeightMap []float64) { + if vizNode.Def != nil { + prevCollectedMaxHeight := layerHeightMap[vizNode.Layer] + if prevCollectedMaxHeight == -1.0 || prevCollectedMaxHeight < vizNode.NodeH { + layerHeightMap[vizNode.Layer] = vizNode.NodeH + } + } + + for _, childItem := range vizNode.PriChildrenAndEnclosedRoots { + populateLayerHeightsRecursive(childItem, layerHeightMap) + } +} + +func populateNodeYCoordRecursive(vizNode *VizNode, layerYCoords []float64) { + if vizNode.Def != nil { + vizNode.Y = layerYCoords[vizNode.Layer] + } + for _, childItem := range vizNode.PriChildrenAndEnclosedRoots { + populateNodeYCoordRecursive(childItem, layerYCoords) + } +} + +func (vnh *VizNodeHierarchy) PopulateNodesYCoords() { + // First, assign all level heights recursively + layerHeightMap := slices.Repeat([]float64{-1.0}, vnh.TotalLayers) + populateLayerHeightsRecursive(&vnh.VizNodeMap[0], layerHeightMap) + + // Second, figure out the Y coord for each layer + layerYCoords := make([]float64, len(layerHeightMap)) + curY := 0.0 + for i := range len(layerHeightMap) { + layerYCoords[i] = curY + if i < len(layerHeightMap)-1 { + curY += layerHeightMap[i] + vnh.UpperLayerGapMap[i+1] + } + } + + // Third, assign those Y coords to nodes recursively + populateNodeYCoordRecursive(&vnh.VizNodeMap[0], layerYCoords) +} + +func getSecOffsetX(startX float64, endX float64, offset float64) (float64, float64) { + if startX-offset >= endX+offset { + // Offset regions do not intersect, upper node is way right + return -offset, offset + } else if startX+offset <= endX-offset { + // Offset regions do not intersect, upper node is way left + return offset, -offset + } else if startX > endX { + // Intersect: left to left + return -offset, -offset + } + // Intersect: right to right + return SecEdgeOffsetX, SecEdgeOffsetX +} + +func (vnh *VizNodeHierarchy) PopulateEdgeLabelCoords() { + for i := range len(vnh.VizNodeMap) - 1 { + dstVizNode := &vnh.VizNodeMap[i+1] + + // Pri edge + if dstVizNode.Def.PriIn.SrcId != 0 { + srcHiearchyItem := &vnh.VizNodeMap[dstVizNode.Def.PriIn.SrcId] + startX := srcHiearchyItem.X + srcHiearchyItem.TotalW/2 + startY := srcHiearchyItem.Y + srcHiearchyItem.NodeH + endX := dstVizNode.X + dstVizNode.TotalW/2 + endY := dstVizNode.Y + deltaX := endX - startX + deltaY := endY - startY + for i := range len(dstVizNode.IncomingVizEdges) { + if dstVizNode.IncomingVizEdges[i].Edge.SrcId == dstVizNode.Def.PriIn.SrcId { + labelCenterY := endY - vnh.EdgeFo.SizeInPixels*2 - dstVizNode.IncomingVizEdges[i].H/2 + labelCenterX := endX - (endY-labelCenterY)*deltaX/deltaY + dstVizNode.IncomingVizEdges[i].Y = labelCenterY - dstVizNode.IncomingVizEdges[i].H/2 + dstVizNode.IncomingVizEdges[i].X = labelCenterX - dstVizNode.IncomingVizEdges[i].W/2 + } + } + } + + // Sec edges + for _, edge := range dstVizNode.Def.SecIn { + srcHiearchyItem := &vnh.VizNodeMap[edge.SrcId] + + startX := srcHiearchyItem.X + srcHiearchyItem.TotalW/2.0 + endX := dstVizNode.X + dstVizNode.TotalW/2.0 + startOffset, endOffset := getSecOffsetX(startX, endX, vnh.NodeFo.SizeInPixels/2.0) + startX += startOffset + endX += endOffset + + startY := srcHiearchyItem.Y + srcHiearchyItem.NodeH + endY := dstVizNode.Y + if dstVizNode.Layer > srcHiearchyItem.Layer+1 { + // Adjust position: we want to put the label between the srcLayer and srcLayer+1 + trueEndY := startY + vnh.UpperLayerGapMap[srcHiearchyItem.Layer+1] + endX = startX + (endX-startX)*(trueEndY-startY)/(endY-startY) + endY = trueEndY + } + deltaX := endX - startX + deltaY := endY - startY + for i := range len(dstVizNode.IncomingVizEdges) { + if dstVizNode.IncomingVizEdges[i].Edge.SrcId == edge.SrcId { + labelCenterY := startY + vnh.EdgeFo.SizeInPixels*5 + dstVizNode.IncomingVizEdges[i].H/2 + labelCenterX := startX + (labelCenterY-startY)*deltaX/deltaY + dstVizNode.IncomingVizEdges[i].Y = labelCenterY - dstVizNode.IncomingVizEdges[i].H/2 + dstVizNode.IncomingVizEdges[i].X = labelCenterX - dstVizNode.IncomingVizEdges[i].W/2 + } + } + } + } +} + +func (vnh *VizNodeHierarchy) RemoveDuplicateSecEdgeLabels() { + secLabelsFromItemMap := map[int16]map[string][]*VizEdge{} // Fight srcid->secText duplicates + for i := range len(vnh.VizNodeMap) - 1 { + vizNode := vnh.VizNodeMap[i+1] + for j := range vizNode.IncomingVizEdges { + e := &vizNode.IncomingVizEdges[j] + if e.HierarchyType != HierarchySec { + continue + } + edgeTextMap, ok := secLabelsFromItemMap[e.Edge.SrcId] + if !ok { + edgeTextMap = map[string][]*VizEdge{} + secLabelsFromItemMap[e.Edge.SrcId] = edgeTextMap + } + _, ok = edgeTextMap[e.Edge.Text] + if !ok { + edgeTextMap[e.Edge.Text] = make([]*VizEdge, 0, 10) + } + edgeTextMap[e.Edge.Text] = append(edgeTextMap[e.Edge.Text], e) + } + } + + for _, edgeTextMap := range secLabelsFromItemMap { + for _, edges := range edgeTextMap { + sort.Slice(edges, func(i int, j int) bool { + return edges[i].X < edges[j].X + }) + i := 0 + j := 1 + for i < len(edges) { + for i < j && j < len(edges) { + if edges[i].X+edges[i].W > edges[j].X { + // We have an intersection, remove j + edges[j].W = 0.0 + edges[j].H = 0.0 + edges = slices.Delete(edges, j, j+1) + } else { + j++ + } + } + i++ + j = i + 1 + } + } + } + +} + +func getBestHierarchy(nodeDefs []NodeDef, nodeFo FontOptions, edgeFo FontOptions, optimize bool) ([]VizNode, int64, float64, float64, error) { + priParentMap := buildPriParentMap(nodeDefs) + layerMap := buildLayerMap(nodeDefs) + rootNodes := buildRootNodeList(priParentMap) + mx, err := NewLayerMx(nodeDefs, layerMap, rootNodes) + if err != nil { + return nil, int64(0), 0.0, 0.0, err + } + + vnh := NewVizNodeHierarchy(nodeDefs, nodeFo, edgeFo) + + vnh.buildNewRootSubtreeHierarchy(mx) + + var bestMx LayerMx + mxPermCnt := 0 + bestDistSec := math.MaxFloat64 + var tElapsed float64 + if optimize { + bestSignature := "z" + tStart := time.Now() + mxi, err := NewLayerMxPermIterator(nodeDefs, mx) + if err != nil { + return nil, int64(0), 0.0, 0.0, err + } + mxi.MxIterator(func(_ int, mxPerm LayerMx) { + + // Hierarchy + vnh.reuseRootSubtreeHierarchy(mxPerm) + + // X coord + vnh.PopulateNodeTotalWidth() + vnh.PopulateNodesXCoords() + + distSec := vnh.CalculateTotalHorizontalShift() + if distSec < bestDistSec { + // This: 1. Adds determinism 2. helps user choose ids that go first (to some extent) + signature := mxPerm.signature() + if distSec < bestDistSec-0.1 || signature < bestSignature { + bestDistSec = distSec + bestMx = mxPerm.clone() + bestSignature = signature + } + } + mxPermCnt++ + }) + tElapsed = time.Since(tStart).Seconds() + + if bestMx == nil { + return nil, int64(mxPermCnt), tElapsed, 0.0, fmt.Errorf("no best") + } + } else { + bestMx = mx + bestDistSec = 0.0 + } + + // Hierarchy + vnh.reuseRootSubtreeHierarchy(bestMx) + + // X coord + vnh.PopulateNodeTotalWidth() + vnh.PopulateNodesXCoords() + + // Y coord + vnh.PopulateEdgeLabelDimensions() + vnh.PopulateUpperLayerGapMap(edgeFo.SizeInPixels) + vnh.PopulateNodesYCoords() + + // Edge label X and Y + vnh.PopulateEdgeLabelCoords() + vnh.RemoveDuplicateSecEdgeLabels() + + return vnh.VizNodeMap, int64(mxPermCnt), tElapsed, bestDistSec, nil +} diff --git a/pkg/capigraph/viz_node_hierarchy_test.go b/pkg/capigraph/viz_node_hierarchy_test.go new file mode 100644 index 00000000..df6cbeab --- /dev/null +++ b/pkg/capigraph/viz_node_hierarchy_test.go @@ -0,0 +1,76 @@ +package capigraph + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBasicMx(t *testing.T) { + mx := LayerMx{{1, 3}, {2}} + + vnh := NewVizNodeHierarchy(testNodeDefsBasic, DefaultNodeFontOptions(), DefaultEdgeLabelFontOptions()) + + vnh.buildNewRootSubtreeHierarchy(mx) + vnh.reuseRootSubtreeHierarchy(mx) + vnh.PopulateNodeTotalWidth() + + assert.Equal(t, 124.0, vnh.VizNodeMap[0].TotalW) + assert.Equal(t, 0.0, vnh.VizNodeMap[0].NodeW) + assert.Equal(t, 0.0, vnh.VizNodeMap[0].NodeH) + + assert.Equal(t, 52.0, vnh.VizNodeMap[1].TotalW) + assert.Equal(t, 52.0, vnh.VizNodeMap[1].NodeW) + assert.Equal(t, 60.0, vnh.VizNodeMap[1].NodeH) + + assert.Equal(t, 52.0, vnh.VizNodeMap[2].TotalW) + assert.Equal(t, 52.0, vnh.VizNodeMap[2].NodeW) + assert.Equal(t, 60.0, vnh.VizNodeMap[2].NodeH) + + assert.Equal(t, 52.0, vnh.VizNodeMap[3].TotalW) + assert.Equal(t, 52.0, vnh.VizNodeMap[3].NodeW) + assert.Equal(t, 60.0, vnh.VizNodeMap[3].NodeH) + + vnh.PopulateNodesXCoords() + + assert.Equal(t, 0.0, vnh.VizNodeMap[0].X) + assert.Equal(t, 0.0, vnh.VizNodeMap[1].X) + assert.Equal(t, 0.0, vnh.VizNodeMap[2].X) + assert.Equal(t, 72.0, vnh.VizNodeMap[3].X) + + horShift := vnh.CalculateTotalHorizontalShift() + assert.Equal(t, 72.0, math.Round(horShift*100)/100.0) + + vnh.PopulateEdgeLabelDimensions() + + assert.Equal(t, int16(1), vnh.VizNodeMap[2].IncomingVizEdges[0].Edge.SrcId) + assert.Equal(t, HierarchyPri, vnh.VizNodeMap[2].IncomingVizEdges[0].HierarchyType) + assert.Equal(t, 0.0, vnh.VizNodeMap[2].IncomingVizEdges[0].W) // No label text + assert.Equal(t, 0.0, vnh.VizNodeMap[2].IncomingVizEdges[0].H) // No label text + + assert.Equal(t, int16(3), vnh.VizNodeMap[2].IncomingVizEdges[1].Edge.SrcId) + assert.Equal(t, HierarchySec, vnh.VizNodeMap[2].IncomingVizEdges[1].HierarchyType) + assert.Equal(t, 69.12, vnh.VizNodeMap[2].IncomingVizEdges[1].W) + assert.Equal(t, 36.0, vnh.VizNodeMap[2].IncomingVizEdges[1].H) + + vnh.PopulateUpperLayerGapMap(DefaultEdgeLabelFontOptions().SizeInPixels) + + assert.Equal(t, 60.0, vnh.UpperLayerGapMap[0]) // Changed + assert.Equal(t, 216.0, vnh.UpperLayerGapMap[1]) + + vnh.PopulateNodesYCoords() + + assert.Equal(t, 0.0, vnh.VizNodeMap[0].Y) + assert.Equal(t, 0.0, vnh.VizNodeMap[1].Y) + assert.Equal(t, 276.0, vnh.VizNodeMap[2].Y) + assert.Equal(t, 0.0, vnh.VizNodeMap[3].Y) + + vnh.PopulateEdgeLabelCoords() + + assert.Equal(t, 26.0, vnh.VizNodeMap[2].IncomingVizEdges[0].X) + assert.Equal(t, 240.0, vnh.VizNodeMap[2].IncomingVizEdges[0].Y) + + assert.Equal(t, 27.44, math.Round(vnh.VizNodeMap[2].IncomingVizEdges[1].X*100.0)/100.0) + assert.Equal(t, 150.0, vnh.VizNodeMap[2].IncomingVizEdges[1].Y) +} diff --git a/pkg/custom/tag_and_denormalize/tag_and_denormalize.go b/pkg/custom/tag_and_denormalize/tag_and_denormalize.go index 666a9276..4037a8c6 100644 --- a/pkg/custom/tag_and_denormalize/tag_and_denormalize.go +++ b/pkg/custom/tag_and_denormalize/tag_and_denormalize.go @@ -20,7 +20,7 @@ const ProcessorTagAndDenormalizeName string = "tag_and_denormalize" type TagAndDenormalizeProcessorDef struct { TagFieldName string `json:"tag_field_name" env:"CAPI_TAGANDDENORMALIZE_TAG_FIELD_NAME, overwrite"` RawTagCriteria map[string]string `json:"tag_criteria" env:"CAPI_TAGANDDENORMALIZE_TAG_CRITERIA, overwrite"` - RawTagCriteriaUri string `json:"tag_criteria_uri" env:"CAPI_TAGANDDENORMALIZE_RAW_TAG_CRITERIA_URI, overwrite"` + RawTagCriteriaUrl string `json:"tag_criteria_url" env:"CAPI_TAGANDDENORMALIZE_RAW_TAG_CRITERIA_URL, overwrite"` ParsedTagCriteria map[string]ast.Expr UsedInCriteriaFields sc.FieldRefs } @@ -58,23 +58,23 @@ func (procDef *TagAndDenormalizeProcessorDef) Deserialize(raw json.RawMessage, _ errors := make([]string, 0) procDef.ParsedTagCriteria = map[string]ast.Expr{} - if len(procDef.RawTagCriteriaUri) > 0 { + if len(procDef.RawTagCriteriaUrl) > 0 { if len(procDef.RawTagCriteria) > 0 { return fmt.Errorf("cannot unmarshal both tag_criteria and tag_criteria_url - pick one") } - criteriaBytes, err := xfer.GetFileBytes(procDef.RawTagCriteriaUri, caPath, privateKeys) + criteriaBytes, err := xfer.GetFileBytes(procDef.RawTagCriteriaUrl, caPath, privateKeys) if err != nil { - return fmt.Errorf("cannot get criteria file [%s]: %s", procDef.RawTagCriteriaUri, err.Error()) + return fmt.Errorf("cannot get criteria file [%s]: %s", procDef.RawTagCriteriaUrl, err.Error()) } if len(criteriaBytes) == 0 { - return fmt.Errorf("criteria file [%s] is empty", procDef.RawTagCriteriaUri) + return fmt.Errorf("criteria file [%s] is empty", procDef.RawTagCriteriaUrl) } if criteriaBytes != nil { if err := json.Unmarshal(criteriaBytes, &procDef.RawTagCriteria); err != nil { - return fmt.Errorf("cannot unmarshal tag criteria file [%s]: [%s]", procDef.RawTagCriteriaUri, err.Error()) + return fmt.Errorf("cannot unmarshal tag criteria file [%s]: [%s]", procDef.RawTagCriteriaUrl, err.Error()) } } } else if len(procDef.RawTagCriteria) == 0 { diff --git a/pkg/custom/tag_and_denormalize/tag_and_denormalize_test.go b/pkg/custom/tag_and_denormalize/tag_and_denormalize_test.go index 48505672..a71eb65f 100644 --- a/pkg/custom/tag_and_denormalize/tag_and_denormalize_test.go +++ b/pkg/custom/tag_and_denormalize/tag_and_denormalize_test.go @@ -150,7 +150,7 @@ func TestTagAndDenormalizeDeserializeFileCriteria(t *testing.T) { re := regexp.MustCompile(`"tag_criteria": \{[^\}]+\}`) err := scriptDef.Deserialize( - []byte(re.ReplaceAllString(scriptJson, `"tag_criteria_uri": "../../../test/data/cfg/tag_and_denormalize_quicktest/tag_criteria.json"`)), sc.ScriptJson, + []byte(re.ReplaceAllString(scriptJson, `"tag_criteria_url": "../../../test/data/cfg/tag_and_denormalize_quicktest/tag_criteria.json"`)), sc.ScriptJson, &TagAndDenormalizeTestTestProcessorDefFactory{}, map[string]json.RawMessage{"tag_and_denormalize": {}}, "", nil) assert.Nil(t, err) @@ -282,12 +282,12 @@ func TestTagAndDenormalizeDeserializeFailures(t *testing.T) { assert.Contains(t, err.Error(), "cannot unmarshal with tag_criteria and tag_criteria_url missing") err = scriptDef.Deserialize( - []byte(re.ReplaceAllString(scriptJson, `"tag_criteria":{"a":"b"},"tag_criteria_uri":"aaa"`)), sc.ScriptJson, + []byte(re.ReplaceAllString(scriptJson, `"tag_criteria":{"a":"b"},"tag_criteria_url":"aaa"`)), sc.ScriptJson, &TagAndDenormalizeTestTestProcessorDefFactory{}, map[string]json.RawMessage{"tag_and_denormalize": {}}, "", nil) assert.Contains(t, err.Error(), "cannot unmarshal both tag_criteria and tag_criteria_url - pick one") err = scriptDef.Deserialize( - []byte(re.ReplaceAllString(scriptJson, `"tag_criteria_uri":"aaa"`)), sc.ScriptJson, + []byte(re.ReplaceAllString(scriptJson, `"tag_criteria_url":"aaa"`)), sc.ScriptJson, &TagAndDenormalizeTestTestProcessorDefFactory{}, map[string]json.RawMessage{"tag_and_denormalize": {}}, "", nil) assert.Contains(t, err.Error(), "cannot get criteria file") diff --git a/pkg/exe/daemon/capidaemon.json b/pkg/exe/daemon/capidaemon.json index 803f3855..9e9948ea 100644 --- a/pkg/exe/daemon/capidaemon.json +++ b/pkg/exe/daemon/capidaemon.json @@ -25,7 +25,7 @@ }, "tag_and_denormalize":{} }, - "thread_pool_size": 2, + "thread_pool_size": 1, "dead_letter_ttl": 10000, "log":{ "level": "debug" diff --git a/pkg/exe/toolbelt/capitoolbelt.go b/pkg/exe/toolbelt/capitoolbelt.go index 38179dbf..dcef5087 100644 --- a/pkg/exe/toolbelt/capitoolbelt.go +++ b/pkg/exe/toolbelt/capitoolbelt.go @@ -26,169 +26,6 @@ import ( "github.com/capillariesio/capillaries/pkg/wfmodel" ) -type DotDiagramType string - -const ( - DotDiagramIndexes DotDiagramType = "indexes" - DotDiagramFields DotDiagramType = "fields" - DotDiagramRunStatus DotDiagramType = "run_status" -) - -func nodeBatchStatusToColor(status wfmodel.NodeBatchStatusType) string { - switch status { - case wfmodel.NodeBatchNone: - return "white" - case wfmodel.NodeBatchStart: - return "lightblue" - case wfmodel.NodeBatchSuccess: - return "green" - case wfmodel.NodeBatchFail: - return "red" - case wfmodel.NodeBatchRunStopReceived: - return "orangered" - default: - return "cyan" - } -} - -func drawFileReader(node *sc.ScriptNodeDef, dotDiagramType DotDiagramType, arrowFontSize int, recordFontSize int) string { - var b strings.Builder - arrowLabelBuilder := strings.Builder{} - if dotDiagramType == DotDiagramType(DotDiagramFields) { - for colName := range node.FileReader.Columns { - arrowLabelBuilder.WriteString(colName) - arrowLabelBuilder.WriteString("\\l") - } - } - fileNames := make([]string, len(node.FileReader.SrcFileUrls)) - copy(fileNames, node.FileReader.SrcFileUrls) - - b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=dotted, fontsize=\"%d\", label=\"%s\"];\n", node.FileReader.SrcFileUrls[0], node.GetTargetName(), arrowFontSize, arrowLabelBuilder.String())) - b.WriteString(fmt.Sprintf("\"%s\" [shape=folder, fontsize=\"%d\", label=\"%s\", tooltip=\"Source data file(s)\"];\n", node.FileReader.SrcFileUrls[0], recordFontSize, strings.Join(fileNames, "\\n"))) - return b.String() -} - -func drawFileCreator(node *sc.ScriptNodeDef, dotDiagramType DotDiagramType, arrowFontSize int, recordFontSize int, allUsedFields sc.FieldRefs, penWidth string, fillColor string, urlEscaper *strings.Replacer, inSrcArrowLabel string) string { - var b strings.Builder - b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=solid, fontsize=\"%d\", label=\"%s\"];\n", node.TableReader.TableName, node.Name, arrowFontSize, inSrcArrowLabel)) - - // Node (file) - b.WriteString(fmt.Sprintf("\"%s\" [shape=record, penwidth=\"%s\", fontsize=\"%d\", fillcolor=\"%s\", style=\"filled\", label=\"{%s|creates file:\\n%s}\", tooltip=\"%s\"];\n", node.Name, penWidth, recordFontSize, fillColor, node.Name, urlEscaper.Replace(node.FileCreator.UrlTemplate), node.Desc)) - - // Out (file) - arrowLabelBuilder := strings.Builder{} - if dotDiagramType == DotDiagramType(DotDiagramFields) { - for i := 0; i < len(allUsedFields); i++ { - arrowLabelBuilder.WriteString(allUsedFields[i].FieldName) - arrowLabelBuilder.WriteString("\\l") - } - } - - b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=dotted, fontsize=\"%d\", label=\"%s\"];\n", node.Name, node.FileCreator.UrlTemplate, arrowFontSize, arrowLabelBuilder.String())) - b.WriteString(fmt.Sprintf("\"%s\" [shape=note, fontsize=\"%d\", label=\"%s\", tooltip=\"Target data file(s)\"];\n", node.FileCreator.UrlTemplate, recordFontSize, node.FileCreator.UrlTemplate)) - return b.String() -} - -func drawTableReader(node *sc.ScriptNodeDef, dotDiagramType DotDiagramType, arrowFontSize int, recordFontSize int, allUsedFields sc.FieldRefs, penWidth string, fillColor string, urlEscaper *strings.Replacer) string { - var b strings.Builder - var inSrcArrowLabel string - if dotDiagramType == DotDiagramType(DotDiagramIndexes) || dotDiagramType == DotDiagramType(DotDiagramRunStatus) { - if node.TableReader.ExpectedBatchesTotal > 1 { - inSrcArrowLabel = fmt.Sprintf("%s (%d batches)", node.TableReader.TableName, node.TableReader.ExpectedBatchesTotal) - } else { - inSrcArrowLabel = fmt.Sprintf("%s (no parallelism)", node.TableReader.TableName) - } - } else if dotDiagramType == DotDiagramType(DotDiagramFields) { - inSrcArrowLabelBuilder := strings.Builder{} - for i := 0; i < len(allUsedFields); i++ { - if allUsedFields[i].TableName == sc.ReaderAlias { - inSrcArrowLabelBuilder.WriteString(allUsedFields[i].FieldName) - inSrcArrowLabelBuilder.WriteString("\\l") - } - } - inSrcArrowLabel = inSrcArrowLabelBuilder.String() - } - if node.HasFileCreator() { - b.WriteString(drawFileCreator(node, dotDiagramType, arrowFontSize, recordFontSize, allUsedFields, penWidth, fillColor, urlEscaper, inSrcArrowLabel)) - } else { - b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=solid, fontsize=\"%d\", label=\"%s\"];\n", node.TableReader.TableName, node.GetTargetName(), arrowFontSize, inSrcArrowLabel)) - } - - if node.HasLookup() { - inLkpArrowLabel := fmt.Sprintf("%s (lookup)", node.Lookup.IndexName) - if dotDiagramType == DotDiagramType(DotDiagramFields) { - inLkpArrowLabelBuilder := strings.Builder{} - for i := 0; i < len(allUsedFields); i++ { - if allUsedFields[i].TableName == sc.LookupAlias { - inLkpArrowLabelBuilder.WriteString(allUsedFields[i].FieldName) - inLkpArrowLabelBuilder.WriteString("\\l") - } - } - inLkpArrowLabel = inLkpArrowLabelBuilder.String() - } - // In (lookup) - b.WriteString(fmt.Sprintf("\"%s\" -> \"%s\" [style=dashed, fontsize=\"%d\", label=\"%s\"];\n", node.Lookup.TableCreator.Name, node.GetTargetName(), arrowFontSize, inLkpArrowLabel)) - } - return b.String() -} - -func drawTableCreator(node *sc.ScriptNodeDef, recordFontSize int, penWidth string, fillColor string) string { - if node.HasLookup() { - return fmt.Sprintf("\"%s\" [shape=record, penwidth=\"%s\", fontsize=\"%d\", fillcolor=\"%s\", style=\"filled\", label=\"{%s|creates table:\\n%s|group:%t, join:%s}\", tooltip=\"%s\"];\n", - node.TableCreator.Name, penWidth, recordFontSize, fillColor, node.Name, node.TableCreator.Name, node.Lookup.IsGroup, node.Lookup.LookupJoin, node.Desc) - } - return fmt.Sprintf("\"%s\" [shape=record, penwidth=\"%s\", fontsize=\"%d\", fillcolor=\"%s\", style=\"filled\", label=\"{%s|creates table:\\n%s}\", tooltip=\"%s\"];\n", - node.TableCreator.Name, penWidth, recordFontSize, fillColor, node.Name, node.TableCreator.Name, node.Desc) -} - -func getDotDiagram(scriptDef *sc.ScriptDef, dotDiagramType DotDiagramType, nodeColorMap map[string]string) string { - var b strings.Builder - - const recordFontSize int = 20 - const arrowFontSize int = 18 - - urlEscaper := strings.NewReplacer(`{`, `\{`, `}`, `\}`, `|`, `\|`) - b.WriteString(fmt.Sprintf("\ndigraph %s {\nrankdir=\"TD\";\n node [fontname=\"Helvetica\"];\nedge [fontname=\"Helvetica\"];\ngraph [splines=true, pad=\"0.5\", ranksep=\"0.5\", nodesep=\"0.5\"];\n", dotDiagramType)) - for _, node := range scriptDef.ScriptNodes { - penWidth := "1" - if node.StartPolicy == sc.NodeStartManual { - penWidth = "6" - } - fillColor := "white" - var ok bool - if nodeColorMap != nil { - if fillColor, ok = nodeColorMap[node.Name]; !ok { - fillColor = "white" // This run does not affect this node, or the node was not started - } - } - - if node.HasFileReader() { - b.WriteString(drawFileReader(node, dotDiagramType, arrowFontSize, recordFontSize)) - } - - allUsedFields := sc.FieldRefs{} - - if node.HasFileCreator() { - usedInAllTargetFileExpressions := node.FileCreator.GetFieldRefsUsedInAllTargetFileExpressions() - allUsedFields.Append(usedInAllTargetFileExpressions) - } else if node.HasTableCreator() { - usedInAllTargetTableExpressions := sc.GetFieldRefsUsedInAllTargetExpressions(node.TableCreator.Fields) - allUsedFields.Append(usedInAllTargetTableExpressions) - } - - if node.HasTableReader() { - b.WriteString(drawTableReader(node, dotDiagramType, arrowFontSize, recordFontSize, allUsedFields, penWidth, fillColor, urlEscaper)) - } - - if node.HasTableCreator() { - b.WriteString(drawTableCreator(node, recordFontSize, penWidth, fillColor)) - } - } - b.WriteString("}\n") - - return b.String() -} - const LogTsFormatUnquoted = `2006-01-02T15:04:05.000-0700` type StandardToolbeltProcessorDefFactory struct { @@ -232,7 +69,7 @@ const ( CmdGetRunHistory string = "get_run_history" CmdGetNodeHistory string = "get_node_history" CmdGetBatchHistory string = "get_batch_history" - CmdGetRunStatusDiagram string = "drop_run_status_diagram" + CmdGetRunStatusDiagram string = "get_run_status_diagram" CmdDropKeyspace string = "drop_keyspace" CmdGetTableCql string = "get_table_cql" CmdProtoFileReaderCreator string = "proto_file_reader_creator" @@ -266,8 +103,8 @@ func validateScript(envConfig *env.EnvConfig) int { validateScriptCmd := flag.NewFlagSet(CmdValidateScript, flag.ExitOnError) scriptFilePath := validateScriptCmd.String("script_file", "", "Path to script file") paramsFilePath := validateScriptCmd.String("params_file", "", "Path to script parameters map file") - isIdxDag := validateScriptCmd.Bool("idx_dag", false, "Print index DAG") - isFieldDag := validateScriptCmd.Bool("field_dag", false, "Print field DAG") + paramsFormat := validateScriptCmd.String("format", "capigraph", "capigraph (default) or dot") + paramsDetail := validateScriptCmd.String("detail", "idx,field", "idx or field or both (default)") if err := validateScriptCmd.Parse(os.Args[2:]); err != nil || *scriptFilePath == "" { usage(validateScriptCmd) return 0 @@ -279,11 +116,10 @@ func validateScript(envConfig *env.EnvConfig) int { return 1 } - if *isIdxDag { - fmt.Println(getDotDiagram(script, DotDiagramIndexes, nil)) - } - if *isFieldDag { - fmt.Println(getDotDiagram(script, DotDiagramFields, nil)) + if *paramsFormat == "capigraph" { + fmt.Println(api.GetCapigraphDiagram(script, strings.Contains(*paramsDetail, "idx"), strings.Contains(*paramsDetail, "field"), true, nil)) + } else { + fmt.Println(api.GetDotDiagram(script, strings.Contains(*paramsDetail, "idx"), strings.Contains(*paramsDetail, "field"), nil)) } return 0 } @@ -491,6 +327,7 @@ func getRunStatusDiagram(envConfig *env.EnvConfig, logger *l.CapiLogger) int { paramsFilePath := getRunStatusDiagramCmd.String("params_file", "", "Path to script parameters map file") keyspace := getRunStatusDiagramCmd.String("keyspace", "", "Keyspace (session id)") runIdString := getRunStatusDiagramCmd.String("run_id", "", "Run id") + paramsFormat := getRunStatusDiagramCmd.String("format", "capigraph", "capigraph (default) or dot") if err := getRunStatusDiagramCmd.Parse(os.Args[2:]); err != nil { usage(getRunStatusDiagramCmd) return 0 @@ -520,12 +357,20 @@ func getRunStatusDiagram(envConfig *env.EnvConfig, logger *l.CapiLogger) int { return 1 } - nodeColorMap := map[string]string{} - for _, node := range nodes { - nodeColorMap[node.ScriptNode] = nodeBatchStatusToColor(node.Status) + if *paramsFormat == "capigraph" { + nodeColorMap := map[string]int32{} + for _, node := range nodes { + nodeColorMap[node.ScriptNode] = api.NodeBatchStatusToCapigraphColor(node.Status) + } + fmt.Println(api.GetCapigraphDiagram(script, false, false, false, nodeColorMap)) + } else { + nodeColorMap := map[string]string{} + for _, node := range nodes { + nodeColorMap[node.ScriptNode] = api.NodeBatchStatusToDotColor(node.Status) + } + fmt.Println(api.GetDotDiagram(script, true, false, nodeColorMap)) } - fmt.Println(getDotDiagram(script, DotDiagramRunStatus, nodeColorMap)) return 0 } diff --git a/pkg/exe/webapi/capiwebapi.go b/pkg/exe/webapi/capiwebapi.go index c6cc7383..ed3ed23b 100644 --- a/pkg/exe/webapi/capiwebapi.go +++ b/pkg/exe/webapi/capiwebapi.go @@ -199,9 +199,9 @@ func (h *UrlHandler) getNodeDesc(scriptCache *expirable.LRU[string, string], log return "", err } - // Now we have script URI, load it + // Now we have script URL, load it - script, _, err := sc.NewScriptFromFiles(scriptCache, h.Env.CaPath, h.Env.PrivateKeys, runProps.ScriptUri, runProps.ScriptParamsUri, h.Env.CustomProcessorDefFactoryInstance, h.Env.CustomProcessorsSettings) + script, _, err := sc.NewScriptFromFiles(scriptCache, h.Env.CaPath, h.Env.PrivateKeys, runProps.ScriptUrl, runProps.ScriptParamsUrl, h.Env.CustomProcessorDefFactoryInstance, h.Env.CustomProcessorsSettings) if err != nil { return "", err } @@ -475,6 +475,121 @@ func (h *UrlHandler) ksRunNodeHistory(w http.ResponseWriter, r *http.Request) { WriteApiSuccess(h.L, &h.Env.Webapi, r, w, result) } +func (h *UrlHandler) ksRunViz(w http.ResponseWriter, r *http.Request) { + var useRootPalette bool + useRootPaletteParam := r.URL.Query().Get("use_root_palette") + if useRootPaletteParam == "true" { + useRootPalette = true + } + + var isStatus bool + isStatusParam := r.URL.Query().Get("is_status") + if isStatusParam == "true" { + isStatus = true + } + + keyspace, err := getField(r, 0) + if err != nil { + WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) + return + } + + cqlSession, err := db.NewSession(h.Env, keyspace, db.DoNotCreateKeyspaceOnConnect) + if err != nil { + WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) + return + } + defer cqlSession.Close() + + runIdString, err := getField(r, 1) + if err != nil { + WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) + return + } + + runId, err := strconv.Atoi(runIdString) + if err != nil { + WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) + return + } + + // Extract script URL from run props + runProps, err := getRunProps(h.L, cqlSession, keyspace, int16(runId)) + if err != nil { + WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) + } + + // Now we have script URL, load it + scriptDef, _, err := sc.NewScriptFromFiles(h.ScriptCache, h.Env.CaPath, h.Env.PrivateKeys, runProps.ScriptUrl, runProps.ScriptParamsUrl, h.Env.CustomProcessorDefFactoryInstance, h.Env.CustomProcessorsSettings) + if err != nil { + WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) + } + + var nodeColorMap map[string]int32 + showIdx := true + showFields := true + if isStatus { + nodeColorMap = map[string]int32{} + nodes, err := api.GetNodeHistoryForRuns(h.L, cqlSession, keyspace, []int16{int16(runId)}) + if err != nil { + WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) + } + for _, node := range nodes { + nodeColorMap[node.ScriptNode] = api.NodeBatchStatusToCapigraphColor(node.Status) + } + showIdx = false + showFields = false + useRootPalette = false + } + + svg := api.GetCapigraphDiagram(scriptDef, showIdx, showFields, useRootPalette, nodeColorMap) + + w.Header().Set("content-type", "image/svg+xml") + w.Header().Set("Access-Control-Allow-Origin", pickAccessControlAllowOrigin(&h.Env.Webapi, r)) + if _, err := w.Write([]byte(svg)); err != nil { + h.L.Error("cannot write svg response, error %s", err.Error()) + } +} + +// func (h *UrlHandler) ksRunStatusViz(w http.ResponseWriter, r *http.Request) { +// keyspace, err := getField(r, 0) +// if err != nil { +// WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) +// return +// } + +// cqlSession, err := db.NewSession(h.Env, keyspace, db.DoNotCreateKeyspaceOnConnect) +// if err != nil { +// WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) +// return +// } +// defer cqlSession.Close() + +// runIdString, err := getField(r, 1) +// if err != nil { +// WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) +// return +// } + +// runId, err := strconv.Atoi(runIdString) +// if err != nil { +// WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) +// return +// } + +// svg, err := h.getViz(h.ScriptCache, h.L, cqlSession, keyspace, int16(runId), true) +// if err != nil { +// WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) +// return +// } + +// w.Header().Set("content-type", "image/svg+xml") +// w.Header().Set("Access-Control-Allow-Origin", pickAccessControlAllowOrigin(&h.Env.Webapi, r)) +// if _, err := w.Write([]byte(svg)); err != nil { +// h.L.Error("cannot write svg response, error %s", err.Error()) +// } +// } + type StartedRunInfo struct { RunId int16 `json:"run_id"` } @@ -523,7 +638,7 @@ func (h *UrlHandler) ksStartRun(w http.ResponseWriter, r *http.Request) { return } - runId, err := api.StartRun(h.Env, h.L, amqpChannel, runProps.ScriptUri, runProps.ScriptParamsUri, cqlSession, keyspace, strings.Split(runProps.StartNodes, ","), runProps.RunDescription) + runId, err := api.StartRun(h.Env, h.L, amqpChannel, runProps.ScriptUrl, runProps.ScriptParamsUrl, cqlSession, keyspace, strings.Split(runProps.StartNodes, ","), runProps.RunDescription) if err != nil { WriteApiError(h.L, &h.Env.Webapi, r, w, r.URL.Path, err, http.StatusInternalServerError) return @@ -685,6 +800,7 @@ func main() { newRoute("GET", "/ks/([a-zA-Z0-9_]+)[/]*", h.ksMatrix), newRoute("GET", "/ks/([a-zA-Z0-9_]+)/run/([0-9]+)/node/([a-zA-Z0-9_]+)/batch_history[/]*", h.ksRunNodeBatchHistory), newRoute("GET", "/ks/([a-zA-Z0-9_]+)/run/([0-9]+)/node_history[/]*", h.ksRunNodeHistory), + newRoute("GET", "/ks/([a-zA-Z0-9_]+)/run/([0-9]+)/viz[/\\?&=a-zA-Z0-9_]*", h.ksRunViz), newRoute("POST", "/ks/([a-zA-Z0-9_]+)/run[/]*", h.ksStartRun), newRoute("OPTIONS", "/ks/([a-zA-Z0-9_]+)/run[/]*", h.ksStartRunOptions), newRoute("DELETE", "/ks/([a-zA-Z0-9_]+)/run/([0-9]+)[/]*", h.ksStopRun), diff --git a/pkg/proc/file_inserter_csv.go b/pkg/proc/file_inserter_csv.go index 6549ec30..0905b9c9 100644 --- a/pkg/proc/file_inserter_csv.go +++ b/pkg/proc/file_inserter_csv.go @@ -18,7 +18,7 @@ func (instr *FileInserter) createCsvFileAndStartWorker(logger *l.CapiLogger, u * var err error var f *os.File - if u.Scheme == xfer.UriSchemeSftp || u.Scheme == xfer.UriSchemeS3 { + if u.Scheme == xfer.UrlSchemeSftp || u.Scheme == xfer.UrlSchemeS3 { f, err = os.CreateTemp("", "capi") if err != nil { return fmt.Errorf("cannot create temp file for %s: %s", instr.FinalFileUrl, err.Error()) diff --git a/pkg/proc/file_inserter_parquet.go b/pkg/proc/file_inserter_parquet.go index 4f5377e0..31eb8a8a 100644 --- a/pkg/proc/file_inserter_parquet.go +++ b/pkg/proc/file_inserter_parquet.go @@ -20,7 +20,7 @@ func (instr *FileInserter) createParquetFileAndStartWorker(logger *l.CapiLogger, var err error var f *os.File - if u.Scheme == xfer.UriSchemeSftp || u.Scheme == xfer.UriSchemeS3 { + if u.Scheme == xfer.UrlSchemeSftp || u.Scheme == xfer.UrlSchemeS3 { f, err = os.CreateTemp("", "capi") if err != nil { return fmt.Errorf("cannot create temp file for %s: %s", instr.FinalFileUrl, err.Error()) diff --git a/pkg/proc/proc_file_creator.go b/pkg/proc/proc_file_creator.go index 7b39a02e..8af3b432 100644 --- a/pkg/proc/proc_file_creator.go +++ b/pkg/proc/proc_file_creator.go @@ -171,7 +171,7 @@ func RunCreateFile(envConfig *env.EnvConfig, u, err := url.Parse(instr.FinalFileUrl) if err != nil { - return BatchStats{RowsRead: 0, RowsWritten: 0}, fmt.Errorf("cannot parse file uri %s: %s", instr.FinalFileUrl, err.Error()) + return BatchStats{RowsRead: 0, RowsWritten: 0}, fmt.Errorf("cannot parse file url %s: %s", instr.FinalFileUrl, err.Error()) } if node.FileCreator.CreatorFileType == sc.CreatorFileTypeCsv { @@ -228,11 +228,11 @@ func RunCreateFile(envConfig *env.EnvConfig, logger.InfoCtx(pCtx, "uploading %s of size %d to %s...", instr.TempFilePath, st.Size(), instr.FinalFileUrl) - if u.Scheme == xfer.UriSchemeSftp { + if u.Scheme == xfer.UrlSchemeSftp { return bs, xfer.UploadSftpFile(instr.TempFilePath, instr.FinalFileUrl, envConfig.PrivateKeys) - } else if u.Scheme == xfer.UriSchemeS3 { + } else if u.Scheme == xfer.UrlSchemeS3 { return bs, xfer.UploadS3File(instr.TempFilePath, u) } - return bs, fmt.Errorf("unexpected URI scheme %s in %s", u.Scheme, instr.FinalFileUrl) + return bs, fmt.Errorf("unexpected URL scheme %s in %s", u.Scheme, instr.FinalFileUrl) } diff --git a/pkg/proc/proc_table_creator.go b/pkg/proc/proc_table_creator.go index 63c67e86..8927f083 100644 --- a/pkg/proc/proc_table_creator.go +++ b/pkg/proc/proc_table_creator.go @@ -89,7 +89,7 @@ func RunReadFileForBatch(envConfig *env.EnvConfig, logger *l.CapiLogger, pCtx *c u, err := url.Parse(filePath) if err != nil { - return bs, fmt.Errorf("cannot parse file uri %s: %s", filePath, err.Error()) + return bs, fmt.Errorf("cannot parse file url %s: %s", filePath, err.Error()) } bs.Src = filePath @@ -98,7 +98,7 @@ func RunReadFileForBatch(envConfig *env.EnvConfig, logger *l.CapiLogger, pCtx *c var localSrcFile *os.File var fileReader io.Reader var fileReadSeeker io.ReadSeeker - if u.Scheme == xfer.UriSchemeFile || len(u.Scheme) == 0 { + if u.Scheme == xfer.UrlSchemeFile || len(u.Scheme) == 0 { localSrcFile, err = os.Open(filePath) if err != nil { return bs, err @@ -106,20 +106,20 @@ func RunReadFileForBatch(envConfig *env.EnvConfig, logger *l.CapiLogger, pCtx *c defer localSrcFile.Close() fileReader = bufio.NewReader(localSrcFile) fileReadSeeker = localSrcFile - } else if u.Scheme == xfer.UriSchemeHttp || u.Scheme == xfer.UriSchemeHttps || u.Scheme == xfer.UriSchemeS3 { + } else if u.Scheme == xfer.UrlSchemeHttp || u.Scheme == xfer.UrlSchemeHttps || u.Scheme == xfer.UrlSchemeS3 { var readCloser io.ReadCloser - if u.Scheme == xfer.UriSchemeHttp || u.Scheme == xfer.UriSchemeHttps { + if u.Scheme == xfer.UrlSchemeHttp || u.Scheme == xfer.UrlSchemeHttps { readCloser, err = xfer.GetHttpReadCloser(filePath, u.Scheme, envConfig.CaPath) if err != nil { return bs, fmt.Errorf("cannot open http file %s: %s", filePath, err.Error()) } - } else if u.Scheme == xfer.UriSchemeS3 { + } else if u.Scheme == xfer.UrlSchemeS3 { readCloser, err = xfer.GetS3ReadCloser(filePath) if err != nil { return bs, fmt.Errorf("cannot open http file %s: %s", filePath, err.Error()) } } else { - return bs, fmt.Errorf("cannot open file %s: unknown uri scheme", filePath) + return bs, fmt.Errorf("cannot open file %s: unknown url scheme", filePath) } defer readCloser.Close() @@ -150,7 +150,7 @@ func RunReadFileForBatch(envConfig *env.EnvConfig, logger *l.CapiLogger, pCtx *c fileReader = readCloser defer readCloser.Close() } - } else if u.Scheme == xfer.UriSchemeSftp { + } else if u.Scheme == xfer.UrlSchemeSftp { // When dealing with sftp, we download the *whole* file, instead of providing a reader dstFile, err := os.CreateTemp("", "capi") if err != nil { @@ -175,7 +175,7 @@ func RunReadFileForBatch(envConfig *env.EnvConfig, logger *l.CapiLogger, pCtx *c fileReader = bufio.NewReader(localSrcFile) fileReadSeeker = localSrcFile } else { - return bs, fmt.Errorf("uri scheme %s not supported: %s", u.Scheme, filePath) + return bs, fmt.Errorf("l scheme %s not supported: %s", u.Scheme, filePath) } if node.FileReader.ReaderFileType == sc.ReaderFileTypeCsv { diff --git a/pkg/sc/script_def_loader.donotcover.go b/pkg/sc/script_def_loader.donotcover.go index 4f7655b7..ae89879d 100644 --- a/pkg/sc/script_def_loader.donotcover.go +++ b/pkg/sc/script_def_loader.donotcover.go @@ -8,41 +8,41 @@ import ( "github.com/hashicorp/golang-lru/v2/expirable" ) -func NewScriptFromFiles(scriptCache *expirable.LRU[string, string], caPath string, privateKeys map[string]string, scriptUri string, scriptParamsUri string, customProcessorDefFactoryInstance CustomProcessorDefFactory, customProcessorsSettings map[string]json.RawMessage) (*ScriptDef, ScriptInitProblemType, error) { +func NewScriptFromFiles(scriptCache *expirable.LRU[string, string], caPath string, privateKeys map[string]string, scriptUrl string, scriptParamsUrl string, customProcessorDefFactoryInstance CustomProcessorDefFactory, customProcessorsSettings map[string]json.RawMessage) (*ScriptDef, ScriptInitProblemType, error) { var err error var jsonBytesScript []byte var jsonBytesParams []byte if scriptCache != nil { - if jsonCachedScript, ok := scriptCache.Get(scriptUri); ok { + if jsonCachedScript, ok := scriptCache.Get(scriptUrl); ok { jsonBytesScript = []byte(jsonCachedScript) } - if scriptParamsUri != "" { - if jsonCachedParams, ok := scriptCache.Get(scriptParamsUri); ok { + if scriptParamsUrl != "" { + if jsonCachedParams, ok := scriptCache.Get(scriptParamsUrl); ok { jsonBytesParams = []byte(jsonCachedParams) } } } if jsonBytesScript == nil { - jsonBytesScript, err = xfer.GetFileBytes(scriptUri, caPath, privateKeys) + jsonBytesScript, err = xfer.GetFileBytes(scriptUrl, caPath, privateKeys) if err != nil { - return nil, ScriptInitConnectivityProblem, fmt.Errorf("cannot read script %s: %s", scriptUri, err.Error()) + return nil, ScriptInitConnectivityProblem, fmt.Errorf("cannot read script %s: %s", scriptUrl, err.Error()) } if scriptCache != nil { - scriptCache.Add(scriptUri, string(jsonBytesScript)) + scriptCache.Add(scriptUrl, string(jsonBytesScript)) } } - if jsonBytesParams == nil && scriptParamsUri != "" { - jsonBytesParams, err = xfer.GetFileBytes(scriptParamsUri, caPath, privateKeys) + if jsonBytesParams == nil && scriptParamsUrl != "" { + jsonBytesParams, err = xfer.GetFileBytes(scriptParamsUrl, caPath, privateKeys) if err != nil { - return nil, ScriptInitConnectivityProblem, fmt.Errorf("cannot read script parameters %s: %s", scriptParamsUri, err.Error()) + return nil, ScriptInitConnectivityProblem, fmt.Errorf("cannot read script parameters %s: %s", scriptParamsUrl, err.Error()) } if scriptCache != nil { - scriptCache.Add(scriptParamsUri, string(jsonBytesParams)) + scriptCache.Add(scriptParamsUrl, string(jsonBytesParams)) } } - return NewScriptFromFileBytes(caPath, privateKeys, scriptUri, jsonBytesScript, scriptParamsUri, jsonBytesParams, customProcessorDefFactoryInstance, customProcessorsSettings) + return NewScriptFromFileBytes(caPath, privateKeys, scriptUrl, jsonBytesScript, scriptParamsUrl, jsonBytesParams, customProcessorDefFactoryInstance, customProcessorsSettings) } diff --git a/pkg/sc/script_def_loader.go b/pkg/sc/script_def_loader.go index 12395a5e..d0bb41e3 100644 --- a/pkg/sc/script_def_loader.go +++ b/pkg/sc/script_def_loader.go @@ -26,9 +26,9 @@ func getFileType(url string) (ScriptType, error) { func NewScriptFromFileBytes( caPath string, privateKeys map[string]string, - scriptUri string, + scriptUrl string, jsonOrYamlBytesScript []byte, - scriptParamsUri string, + scriptParamsUrl string, jsonOrYamlBytesParams []byte, customProcessorDefFactoryInstance CustomProcessorDefFactory, customProcessorsSettings map[string]json.RawMessage) (*ScriptDef, ScriptInitProblemType, error) { @@ -50,19 +50,19 @@ func NewScriptFromFileBytes( re = regexp.MustCompile(`([^"]{[a-zA-Z0-9_]+\|(number|bool)})|({[a-zA-Z0-9_]+\|(number|bool)}[^"])`) invalidParamRefs := re.FindAllString(jsonOrYamlScriptString, -1) if len(invalidParamRefs) > 0 { - return nil, ScriptInitUrlProblem, fmt.Errorf("cannot parse number/bool script parameter references in [%s], the following parameter references should not have extra characters between curly braces and double quotes: [%s]", scriptUri, strings.Join(invalidParamRefs, ",")) + return nil, ScriptInitUrlProblem, fmt.Errorf("cannot parse number/bool script parameter references in [%s], the following parameter references should not have extra characters between curly braces and double quotes: [%s]", scriptUrl, strings.Join(invalidParamRefs, ",")) } // Apply template params here, script def should know nothing about them: they may tweak some 3d-party tfm config paramsMap := map[string]any{} if jsonOrYamlBytesParams != nil { - scriptParamsType, err := getFileType(scriptParamsUri) + scriptParamsType, err := getFileType(scriptParamsUrl) if err != nil { return nil, ScriptInitContentProblem, err } if err := JsonOrYamlUnmarshal(scriptParamsType, jsonOrYamlBytesParams, ¶msMap); err != nil { - return nil, ScriptInitContentProblem, fmt.Errorf("cannot unmarshal script params from [%s]: [%s]", scriptParamsUri, err.Error()) + return nil, ScriptInitContentProblem, fmt.Errorf("cannot unmarshal script params from [%s]: [%s]", scriptParamsUrl, err.Error()) } } @@ -103,16 +103,16 @@ func NewScriptFromFileBytes( for i, itemAny := range arrayParamVal { itemStr, ok := itemAny.(string) if !ok { - return nil, ScriptInitContentProblem, fmt.Errorf("stringlist contains non-string value type %T from [%s]: %s", itemAny, scriptParamsUri, templateParam) + return nil, ScriptInitContentProblem, fmt.Errorf("stringlist contains non-string value type %T from [%s]: %s", itemAny, scriptParamsUrl, templateParam) } strArray[i] = fmt.Sprintf(`"%s"`, itemStr) } replacerStrings[i+1] = fmt.Sprintf("[%s]", strings.Join(strArray, ",")) default: - return nil, ScriptInitContentProblem, fmt.Errorf("unsupported array parameter type %T from [%s]: %s", arrayParamVal, scriptParamsUri, templateParam) + return nil, ScriptInitContentProblem, fmt.Errorf("unsupported array parameter type %T from [%s]: %s", arrayParamVal, scriptParamsUrl, templateParam) } } else { - return nil, ScriptInitContentProblem, fmt.Errorf("unsupported parameter type %T from [%s]: %s", templateParamVal, scriptParamsUri, templateParam) + return nil, ScriptInitContentProblem, fmt.Errorf("unsupported parameter type %T from [%s]: %s", templateParamVal, scriptParamsUrl, templateParam) } } i += 2 @@ -130,17 +130,17 @@ func NewScriptFromFileBytes( } } if len(unresolvedParamMap) > 0 { - return nil, ScriptInitContentProblem, fmt.Errorf("unresolved parameter references in [%s]: %v; make sure that type in the script matches the type of the parameter value in the script parameters file", scriptUri, unresolvedParamMap) + return nil, ScriptInitContentProblem, fmt.Errorf("unresolved parameter references in [%s]: %v; make sure that type in the script matches the type of the parameter value in the script parameters file", scriptUrl, unresolvedParamMap) } - scriptType, err := getFileType(scriptUri) + scriptType, err := getFileType(scriptUrl) if err != nil { return nil, ScriptInitContentProblem, err } newScript := &ScriptDef{} if err := newScript.Deserialize([]byte(jsonOrYamlScriptString), scriptType, customProcessorDefFactoryInstance, customProcessorsSettings, caPath, privateKeys); err != nil { - return nil, ScriptInitContentProblem, fmt.Errorf("cannot deserialize script %s(%s): %s", scriptUri, scriptParamsUri, err.Error()) + return nil, ScriptInitContentProblem, fmt.Errorf("cannot deserialize script %s(%s): %s", scriptUrl, scriptParamsUrl, err.Error()) } return newScript, ScriptInitNoProblem, nil diff --git a/pkg/sc/script_def_loader_test.go b/pkg/sc/script_def_loader_test.go index eec92c37..c70771e5 100644 --- a/pkg/sc/script_def_loader_test.go +++ b/pkg/sc/script_def_loader_test.go @@ -238,7 +238,7 @@ func (f *SomeTestCustomProcessorDefFactory) Create(processorType string) (Custom func TestNewScriptFromFileBytes(t *testing.T) { // Test main script parsing function scriptDef, initProblem, err := NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(parameterizedScriptJson), + "someScriptUrl.json", []byte(parameterizedScriptJson), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Nil(t, err) @@ -255,68 +255,68 @@ func TestNewScriptFromFileBytes(t *testing.T) { // Tweak paramater name and make sure templating engine catches it _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "source_table_for_test_custom_processor", "some_bad_param")), + "someScriptUrl.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "source_table_for_test_custom_processor", "some_bad_param")), "someScriptParamsUrl.json", []byte(paramsJson), nil, nil) assert.Contains(t, err.Error(), "unresolved parameter references", err.Error()) // Bad-formed JSON _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(strings.TrimSuffix(parameterizedScriptJson, "}")), + "someScriptUrl.json", []byte(strings.TrimSuffix(parameterizedScriptJson, "}")), "someScriptParamsUrl.json", []byte(paramsJson), nil, nil) assert.Contains(t, err.Error(), "unexpected end of JSON input", err.Error()) // Invalid field in custom processor (Python) formula _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "-r.field_int1*2", "r.bad_field")), + "someScriptUrl.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "-r.field_int1*2", "r.bad_field")), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "field usage error in custom processor creator") // Invalid dependency policy _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "run_is_current(desc),node_start_ts(desc)", "some_bad_event_priority_order")), + "someScriptUrl.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "run_is_current(desc),node_start_ts(desc)", "some_bad_event_priority_order")), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "failed to deserialize dependency policy") // Run (tweaked) dependency policy checker with some vanilla values and see if it works _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "e.run_final_status == wfmodel.RunStart", "e.run_final_status == true")), + "someScriptUrl.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "e.run_final_status == wfmodel.RunStart", "e.run_final_status == true")), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "failed to test dependency policy") re := regexp.MustCompile(`"expression": "e\.run[^"]+"`) _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(re.ReplaceAllString(parameterizedScriptJson, `"expression": 1`)), + "someScriptUrl.json", []byte(re.ReplaceAllString(parameterizedScriptJson, `"expression": 1`)), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "cannot unmarshal dependency policy") _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(re.ReplaceAllString(parameterizedScriptJson, `"expression": "a.aaa"`)), + "someScriptUrl.json", []byte(re.ReplaceAllString(parameterizedScriptJson, `"expression": "a.aaa"`)), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "cannot parse rule expression 'a.aaa': all fields must be prefixed") _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(re.ReplaceAllString(parameterizedScriptJson, `"expression": "e.aaa"`)), + "someScriptUrl.json", []byte(re.ReplaceAllString(parameterizedScriptJson, `"expression": "e.aaa"`)), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "cannot parse rule expression 'e.aaa': field e.aaa not found") // Tweak lookup isGroup = false and get error _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(parameterizedScriptJson), + "someScriptUrl.json", []byte(parameterizedScriptJson), "someScriptParamsUrl.json", []byte(strings.ReplaceAll(paramsJson, "true", "false")), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "cannot use agg functions") // Invalid rerun_policy _, _, err = NewScriptFromFileBytes("", nil, - "someScriptUri.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "\"rerun_policy\": \"fail\"", "\"rerun_policy\": \"bad_rerun_policy\"")), + "someScriptUrl.json", []byte(strings.ReplaceAll(parameterizedScriptJson, "\"rerun_policy\": \"fail\"", "\"rerun_policy\": \"bad_rerun_policy\"")), "someScriptParamsUrl.json", []byte(paramsJson), &SomeTestCustomProcessorDefFactory{}, map[string]json.RawMessage{"some_test_custom_proc": []byte("{}")}) assert.Contains(t, err.Error(), "invalid node rerun policy bad_rerun_policy") diff --git a/pkg/wf/message_handler.go b/pkg/wf/message_handler.go index 97518f52..4af92481 100644 --- a/pkg/wf/message_handler.go +++ b/pkg/wf/message_handler.go @@ -137,7 +137,7 @@ func SafeProcessBatch(envConfig *env.EnvConfig, logger *l.CapiLogger, pCtx *ctx. if err != nil { logger.DebugCtx(pCtx, "batch processed, error: %s", err.Error()) - return wfmodel.NodeBatchFail, bs, fmt.Errorf("error running node %s of type %s in the script [%s]: [%s]", pCtx.BatchInfo.TargetNodeName, pCtx.CurrentScriptNode.Type, pCtx.BatchInfo.ScriptURI, err.Error()) + return wfmodel.NodeBatchFail, bs, fmt.Errorf("error running node %s of type %s in the script [%s]: [%s]", pCtx.BatchInfo.TargetNodeName, pCtx.CurrentScriptNode.Type, pCtx.BatchInfo.ScriptURL, err.Error()) } logger.DebugCtx(pCtx, "batch processed ok") @@ -227,14 +227,14 @@ func initCtxScript(logger *l.CapiLogger, scriptCache *expirable.LRU[string, stri var initProblem sc.ScriptInitProblemType var err error - pCtx.Script, initProblem, err = sc.NewScriptFromFiles(scriptCache, caPath, privateKeys, dataBatchInfo.ScriptURI, dataBatchInfo.ScriptParamsURI, customProcFactory, customProcSettings) + pCtx.Script, initProblem, err = sc.NewScriptFromFiles(scriptCache, caPath, privateKeys, dataBatchInfo.ScriptURL, dataBatchInfo.ScriptParamsURL, customProcFactory, customProcSettings) if initProblem == sc.ScriptInitNoProblem { return DaemonCmdNone } switch initProblem { case sc.ScriptInitUrlProblem: - logger.Error("cannot init script because of URI problem, will not let other workers handle it, giving up with msg %s: %s", dataBatchInfo.ToString(), err.Error()) + logger.Error("cannot init script because of URL problem, will not let other workers handle it, giving up with msg %s: %s", dataBatchInfo.ToString(), err.Error()) return DaemonCmdAckWithError case sc.ScriptInitContentProblem: logger.Error("cannot init script because of content problem, will not let other workers handle it, giving up with msg %s: %s", dataBatchInfo.ToString(), err.Error()) @@ -404,7 +404,7 @@ func ProcessDataBatchMsg(envConfig *env.EnvConfig, logger *l.CapiLogger, scriptC var ok bool pCtx.CurrentScriptNode, ok = pCtx.Script.ScriptNodes[dataBatchInfo.TargetNodeName] if !ok { - logger.Error("cannot find node %s in the script [%s], giving up with %s, returning DaemonCmdAckWithError, will not let other workers handle it", pCtx.BatchInfo.TargetNodeName, pCtx.BatchInfo.ScriptURI, dataBatchInfo.ToString()) + logger.Error("cannot find node %s in the script [%s], giving up with %s, returning DaemonCmdAckWithError, will not let other workers handle it", pCtx.BatchInfo.TargetNodeName, pCtx.BatchInfo.ScriptURL, dataBatchInfo.ToString()) return DaemonCmdAckWithError } diff --git a/pkg/wfdb/dependency_node_event.go b/pkg/wfdb/dependency_node_event.go index d488c303..a231b590 100644 --- a/pkg/wfdb/dependency_node_event.go +++ b/pkg/wfdb/dependency_node_event.go @@ -35,7 +35,7 @@ func BuildDependencyNodeEventLists(logger *l.CapiLogger, pCtx *ctx.MessageProces for _, affectingRunId := range nodeAffectingRunIdsMap[nodeName] { runLifespan, ok := runLifespanMap[affectingRunId] if !ok { - return nil, fmt.Errorf("unexpectedly, cannot find run lifespan map for run %d: %s", affectingRunId, runLifespanMap.ToString()) + return nil, fmt.Errorf("unexpectedly, cannot find run lifespan map for run %d, was it ever started?", affectingRunId) } if runLifespan.StartTs == time.Unix(0, 0) || runLifespan.FinalStatus == wfmodel.RunNone { return nil, fmt.Errorf("unexpectedly, run lifespan %d looks like the run never started: %s", affectingRunId, runLifespanMap.ToString()) diff --git a/pkg/wfdb/run_properties.go b/pkg/wfdb/run_properties.go index 00c036ff..cd8e8bd6 100644 --- a/pkg/wfdb/run_properties.go +++ b/pkg/wfdb/run_properties.go @@ -100,14 +100,14 @@ func HarvestRunIdsByAffectedNodes(logger *l.CapiLogger, pCtx *ctx.MessageProcess return runIds, nodeAffectingRunIdsMap, nil } -func WriteRunProperties(cqlSession *gocql.Session, keyspace string, runId int16, startNodes []string, affectedNodes []string, scriptUri string, scriptParamsUri string, runDescription string) error { +func WriteRunProperties(cqlSession *gocql.Session, keyspace string, runId int16, startNodes []string, affectedNodes []string, scriptUrl string, scriptParamsUrl string, runDescription string) error { q := (&cql.QueryBuilder{}). Keyspace(keyspace). Write("run_id", runId). Write("start_nodes", strings.Join(startNodes, ",")). Write("affected_nodes", strings.Join(affectedNodes, ",")). - Write("script_uri", scriptUri). - Write("script_params_uri", scriptParamsUri). + Write("script_url", scriptUrl). + Write("script_params_url", scriptParamsUrl). Write("run_description", runDescription). InsertUnpreparedQuery(wfmodel.TableNameRunAffectedNodes, cql.IgnoreIfExists) // If not exists. First one wins. err := cqlSession.Query(q).Exec() diff --git a/pkg/wfmodel/message.go b/pkg/wfmodel/message.go index 9189f4c7..85021527 100644 --- a/pkg/wfmodel/message.go +++ b/pkg/wfmodel/message.go @@ -92,8 +92,8 @@ func (msg *Message) Deserialize(jsonBytes []byte) error { // tgtMsg.Ts = time.Now().UnixMilli() // tgtMsg.MessageType = MessageTypeDataBatch // tgtMsg.Payload = MessagePayloadDataBatch{ -// ScriptURI: context.BatchInfo.ScriptURI, -// ScriptParamsURI: context.BatchInfo.ScriptParamsURI, +// ScriptURL: context.BatchInfo.ScriptURL, +// ScriptParamsURL: context.BatchInfo.ScriptParamsURL, // DataKeyspace: context.BatchInfo.DataKeyspace, // RunId: context.BatchInfo.RunId, // TargetNodeName: targetNodeName, diff --git a/pkg/wfmodel/message_payload_data_batch.go b/pkg/wfmodel/message_payload_data_batch.go index 0728bcae..650ee06b 100644 --- a/pkg/wfmodel/message_payload_data_batch.go +++ b/pkg/wfmodel/message_payload_data_batch.go @@ -6,8 +6,8 @@ import ( ) type MessagePayloadDataBatch struct { - ScriptURI string `json:"script_uri"` - ScriptParamsURI string `json:"script_params_uri"` + ScriptURL string `json:"script_url"` + ScriptParamsURL string `json:"script_params_url"` DataKeyspace string `json:"data_keyspace"` // Instance/process id RunId int16 `json:"run_id"` TargetNodeName string `json:"target_node"` @@ -22,8 +22,8 @@ func (dc *MessagePayloadDataBatch) FullBatchId() string { } func (dc *MessagePayloadDataBatch) ToString() string { - return fmt.Sprintf("ScriptURI:%s,ScriptParamsURI:%s, DataKeyspace:%s, RunId:%d, TargetNodeName:%s, FirstToken:%d, LastToken:%d, BatchIdx:%d, BatchesTotal:%d. ", - dc.ScriptURI, dc.ScriptParamsURI, dc.DataKeyspace, dc.RunId, dc.TargetNodeName, dc.FirstToken, dc.LastToken, dc.BatchIdx, dc.BatchesTotal) + return fmt.Sprintf("ScriptURL:%s,ScriptParamsURL:%s, DataKeyspace:%s, RunId:%d, TargetNodeName:%s, FirstToken:%d, LastToken:%d, BatchIdx:%d, BatchesTotal:%d. ", + dc.ScriptURL, dc.ScriptParamsURL, dc.DataKeyspace, dc.RunId, dc.TargetNodeName, dc.FirstToken, dc.LastToken, dc.BatchIdx, dc.BatchesTotal) } func (dc *MessagePayloadDataBatch) Deserialize(jsonBytes []byte) error { diff --git a/pkg/wfmodel/run_properties.go b/pkg/wfmodel/run_properties.go index a34410b9..3dfae1b5 100644 --- a/pkg/wfmodel/run_properties.go +++ b/pkg/wfmodel/run_properties.go @@ -11,13 +11,13 @@ type RunProperties struct { RunId int16 `header:"run_id" format:"%6d" column:"run_id" type:"int" key:"true" json:"run_id"` StartNodes string `header:"start_nodes" format:"%20v" column:"start_nodes" type:"text" json:"start_nodes"` AffectedNodes string `header:"affected_nodes" format:"%20v" column:"affected_nodes" type:"text" json:"affected_nodes"` - ScriptUri string `header:"script_uri" format:"%20v" column:"script_uri" type:"text" json:"script_uri"` - ScriptParamsUri string `header:"script_params_uri" format:"%20v" column:"script_params_uri" type:"text" json:"script_params_uri"` + ScriptUrl string `header:"script_url" format:"%20v" column:"script_url" type:"text" json:"script_url"` + ScriptParamsUrl string `header:"script_params_url" format:"%20v" column:"script_params_url" type:"text" json:"script_params_url"` RunDescription string `header:"run_desc" format:"%20v" column:"run_description" type:"text" json:"run_description"` } func RunPropertiesAllFields() []string { - return []string{"run_id", "start_nodes", "affected_nodes", "script_uri", "script_params_uri", "run_description"} + return []string{"run_id", "start_nodes", "affected_nodes", "script_url", "script_params_url", "run_description"} } func NewRunPropertiesFromMap(r map[string]any, fields []string) (*RunProperties, error) { @@ -31,10 +31,10 @@ func NewRunPropertiesFromMap(r map[string]any, fields []string) (*RunProperties, res.StartNodes, err = ReadStringFromRow(fieldName, r) case "affected_nodes": res.AffectedNodes, err = ReadStringFromRow(fieldName, r) - case "script_uri": - res.ScriptUri, err = ReadStringFromRow(fieldName, r) - case "script_params_uri": - res.ScriptParamsUri, err = ReadStringFromRow(fieldName, r) + case "script_url": + res.ScriptUrl, err = ReadStringFromRow(fieldName, r) + case "script_params_url": + res.ScriptParamsUrl, err = ReadStringFromRow(fieldName, r) case "run_description": res.RunDescription, err = ReadStringFromRow(fieldName, r) default: diff --git a/pkg/xfer/get_file_bytes.go b/pkg/xfer/get_file_bytes.go index fcaa2992..27a6cea8 100644 --- a/pkg/xfer/get_file_bytes.go +++ b/pkg/xfer/get_file_bytes.go @@ -6,37 +6,37 @@ import ( "os" ) -func GetFileBytes(uri string, certPath string, privateKeys map[string]string) ([]byte, error) { - u, err := url.Parse(uri) +func GetFileBytes(fileUrl string, certPath string, privateKeys map[string]string) ([]byte, error) { + u, err := url.Parse(fileUrl) if err != nil { - return nil, fmt.Errorf("cannot parse uri %s: %s", uri, err.Error()) + return nil, fmt.Errorf("cannot parse url %s: %s", fileUrl, err.Error()) } var bytes []byte - if u.Scheme == UriSchemeFile || len(u.Scheme) == 0 { - bytes, err = os.ReadFile(uri) + if u.Scheme == UrlSchemeFile || len(u.Scheme) == 0 { + bytes, err = os.ReadFile(fileUrl) if err != nil { return nil, err } - } else if u.Scheme == UriSchemeHttp || u.Scheme == UriSchemeHttps { - bytes, err = readHttpFile(uri, u.Scheme, certPath) + } else if u.Scheme == UrlSchemeHttp || u.Scheme == UrlSchemeHttps { + bytes, err = readHttpFile(fileUrl, u.Scheme, certPath) if err != nil { return nil, err } - } else if u.Scheme == UriSchemeS3 { - bytes, err = readS3File(uri) + } else if u.Scheme == UrlSchemeS3 { + bytes, err = readS3File(fileUrl) if err != nil { return nil, err } - } else if u.Scheme == UriSchemeSftp { + } else if u.Scheme == UrlSchemeSftp { // When dealing with sftp, we download the *whole* file, then we read all of it dstFile, err := os.CreateTemp("", "capi") if err != nil { - return nil, fmt.Errorf("cannot creeate temp file for %s: %s", uri, err.Error()) + return nil, fmt.Errorf("cannot creeate temp file for %s: %s", fileUrl, err.Error()) } // Download and schedule delete - if err = DownloadSftpFile(uri, privateKeys, dstFile); err != nil { + if err = DownloadSftpFile(fileUrl, privateKeys, dstFile); err != nil { dstFile.Close() return nil, err } @@ -46,10 +46,10 @@ func GetFileBytes(uri string, certPath string, privateKeys map[string]string) ([ // Read bytes, err = os.ReadFile(dstFile.Name()) if err != nil { - return nil, fmt.Errorf("cannot read from file %s downloaded from %s: %s", dstFile.Name(), uri, err.Error()) + return nil, fmt.Errorf("cannot read from file %s downloaded from %s: %s", dstFile.Name(), fileUrl, err.Error()) } } else { - return nil, fmt.Errorf("uri scheme %s not supported: %s", u.Scheme, uri) + return nil, fmt.Errorf("url scheme %s not supported: %s", u.Scheme, fileUrl) } return bytes, nil diff --git a/pkg/xfer/http.go b/pkg/xfer/http.go index f009da25..d678e45a 100644 --- a/pkg/xfer/http.go +++ b/pkg/xfer/http.go @@ -11,18 +11,18 @@ import ( "time" ) -const UriSchemeFile string = "file" -const UriSchemeHttp string = "http" -const UriSchemeHttps string = "https" -const UriSchemeSftp string = "sftp" -const UriSchemeS3 string = "s3" +const UrlSchemeFile string = "file" +const UrlSchemeHttp string = "http" +const UrlSchemeHttps string = "https" +const UrlSchemeSftp string = "sftp" +const UrlSchemeS3 string = "s3" -func GetHttpReadCloser(uri string, scheme string, certDir string) (io.ReadCloser, error) { +func GetHttpReadCloser(fileUrl string, scheme string, certDir string) (io.ReadCloser, error) { var caCertPool *x509.CertPool // tls.Config doc: If RootCAs is nil, TLS uses the host's root CA set. if certDir != "" { caCertPool = x509.NewCertPool() - if scheme == UriSchemeHttps { + if scheme == UrlSchemeHttps { files, err := os.ReadDir(certDir) if err != nil { return nil, fmt.Errorf("cannot read ca dir with PEM certs %s: %s", certDir, err.Error()) @@ -40,21 +40,21 @@ func GetHttpReadCloser(uri string, scheme string, certDir string) (io.ReadCloser t := &http.Transport{TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{}, RootCAs: caCertPool}} client := http.Client{Transport: t, Timeout: 30 * time.Second} - resp, err := client.Get(uri) + resp, err := client.Get(fileUrl) if err != nil { - return nil, fmt.Errorf("cannot get %s: %s", uri, err.Error()) + return nil, fmt.Errorf("cannot get %s: %s", fileUrl, err.Error()) } if resp.StatusCode != http.StatusOK { resp.Body.Close() - return nil, fmt.Errorf("cannot get %s, bad status: %s", uri, resp.Status) + return nil, fmt.Errorf("cannot get %s, bad status: %s", fileUrl, resp.Status) } return resp.Body, nil } -func readHttpFile(uri string, scheme string, certDir string) ([]byte, error) { - r, err := GetHttpReadCloser(uri, scheme, certDir) +func readHttpFile(fileUrl string, scheme string, certDir string) ([]byte, error) { + r, err := GetHttpReadCloser(fileUrl, scheme, certDir) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func readHttpFile(uri string, scheme string, certDir string) ([]byte, error) { bytes, err := io.ReadAll(r) if err != nil { - return nil, fmt.Errorf("cannot read body of %s, bad status: %s", uri, err.Error()) + return nil, fmt.Errorf("cannot read body of %s, bad status: %s", fileUrl, err.Error()) } return bytes, nil } diff --git a/pkg/xfer/s3.go b/pkg/xfer/s3.go index 37c589f6..852d68ec 100644 --- a/pkg/xfer/s3.go +++ b/pkg/xfer/s3.go @@ -14,8 +14,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" ) -func GetS3ReadCloser(uri string) (io.ReadCloser, error) { - parsedUri, _ := url.Parse(uri) +func GetS3ReadCloser(fileUrl string) (io.ReadCloser, error) { + parsedUrl, _ := url.Parse(fileUrl) // Assuming ~/.aws/credentials: // [default] @@ -39,8 +39,8 @@ func GetS3ReadCloser(uri string) (io.ReadCloser, error) { // }) resp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{ - Bucket: aws.String(parsedUri.Host), - Key: aws.String(strings.TrimLeft(parsedUri.Path, "/")), + Bucket: aws.String(parsedUrl.Host), + Key: aws.String(strings.TrimLeft(parsedUrl.Path, "/")), }) if err != nil { return nil, err @@ -49,8 +49,8 @@ func GetS3ReadCloser(uri string) (io.ReadCloser, error) { return resp.Body, nil } -func readS3File(uri string) ([]byte, error) { - r, err := GetS3ReadCloser(uri) +func readS3File(fileUrl string) ([]byte, error) { + r, err := GetS3ReadCloser(fileUrl) if err != nil { return nil, err } @@ -58,7 +58,7 @@ func readS3File(uri string) ([]byte, error) { bytes, err := io.ReadAll(r) if err != nil { - return nil, fmt.Errorf("cannot read body of %s, bad status: %s", uri, err.Error()) + return nil, fmt.Errorf("cannot read body of %s, bad status: %s", fileUrl, err.Error()) } return bytes, nil } diff --git a/pkg/xfer/sftp.go b/pkg/xfer/sftp.go index 76b5df97..68096372 100644 --- a/pkg/xfer/sftp.go +++ b/pkg/xfer/sftp.go @@ -12,7 +12,7 @@ import ( "golang.org/x/crypto/ssh" ) -type ParsedSftpUri struct { +type ParsedSftpUrl struct { User string Host string Port int @@ -22,104 +22,104 @@ type ParsedSftpUri struct { const DefaultSshPort int = 22 -func parseSftpUri(uri string, privateKeys map[string]string) (*ParsedSftpUri, error) { +func parseSftpUrl(fileUrl string, privateKeys map[string]string) (*ParsedSftpUrl, error) { // sftp://user[:pass]@host[:port]/path/to/file - u, err := url.Parse(uri) + u, err := url.Parse(fileUrl) if err != nil { return nil, err } if u.User == nil || u.User.Username() == "" { - return nil, fmt.Errorf("empty username in sftp uri %s", uri) + return nil, fmt.Errorf("empty username in sftp url %s", fileUrl) } userName := u.User.Username() privateKeyPath, ok := privateKeys[userName] if !ok { - return nil, fmt.Errorf("username %s in sftp uri %s not found in environment configuration", userName, uri) + return nil, fmt.Errorf("username %s in sftp url %s not found in environment configuration", userName, fileUrl) } hostParts := strings.Split(u.Host, ":") if len(hostParts) < 2 { - return &ParsedSftpUri{userName, hostParts[0], DefaultSshPort, privateKeyPath, u.Path}, nil + return &ParsedSftpUrl{userName, hostParts[0], DefaultSshPort, privateKeyPath, u.Path}, nil } port, err := strconv.ParseInt(hostParts[1], 10, 32) if err != nil { - return nil, fmt.Errorf("cannot parse port in sftp uri %s", uri) + return nil, fmt.Errorf("cannot parse port in sftp url %s", fileUrl) } - return &ParsedSftpUri{userName, hostParts[0], int(port), privateKeyPath, u.Path}, nil + return &ParsedSftpUrl{userName, hostParts[0], int(port), privateKeyPath, u.Path}, nil } -func DownloadSftpFile(uri string, privateKeys map[string]string, dstFile *os.File) error { - parsedUri, err := parseSftpUri(uri, privateKeys) +func DownloadSftpFile(fileUrl string, privateKeys map[string]string, dstFile *os.File) error { + parsedUrl, err := parseSftpUrl(fileUrl, privateKeys) if err != nil { return err } // Assume empty key password "" - sshClientConfig, err := NewSshClientConfig(parsedUri.User, parsedUri.PrivateKeyPath, "") + sshClientConfig, err := NewSshClientConfig(parsedUrl.User, parsedUrl.PrivateKeyPath, "") if err != nil { return err } - sshUrl := fmt.Sprintf("%s:%d", parsedUri.Host, parsedUri.Port) + sshUrl := fmt.Sprintf("%s:%d", parsedUrl.Host, parsedUrl.Port) sshClient, err := ssh.Dial("tcp", sshUrl, sshClientConfig) if err != nil { - return fmt.Errorf("dial to %s failed: %s", uri, err.Error()) + return fmt.Errorf("dial to %s failed: %s", fileUrl, err.Error()) } defer sshClient.Close() sftpClient, err := sftp.NewClient(sshClient) if err != nil { - return fmt.Errorf("cannot create sftp client to %s: %s", uri, err.Error()) + return fmt.Errorf("cannot create sftp client to %s: %s", fileUrl, err.Error()) } defer sftpClient.Close() - srcFile, err := sftpClient.Open(parsedUri.RemotePath) + srcFile, err := sftpClient.Open(parsedUrl.RemotePath) if err != nil { - return fmt.Errorf("cannot open target file for sftp download %s: %s", uri, err.Error()) + return fmt.Errorf("cannot open target file for sftp download %s: %s", fileUrl, err.Error()) } defer srcFile.Close() _, err = dstFile.ReadFrom(srcFile) if err != nil { - return fmt.Errorf("cannot read for download %s: %s", uri, err.Error()) + return fmt.Errorf("cannot read for download %s: %s", fileUrl, err.Error()) } return nil } -func UploadSftpFile(srcPath string, uri string, privateKeys map[string]string) error { - parsedUri, err := parseSftpUri(uri, privateKeys) +func UploadSftpFile(srcPath string, fileUrl string, privateKeys map[string]string) error { + parsedUrl, err := parseSftpUrl(fileUrl, privateKeys) if err != nil { return err } // Assume empty key password "" - sshClientConfig, err := NewSshClientConfig(parsedUri.User, parsedUri.PrivateKeyPath, "") + sshClientConfig, err := NewSshClientConfig(parsedUrl.User, parsedUrl.PrivateKeyPath, "") if err != nil { return err } - sshUrl := fmt.Sprintf("%s:%d", parsedUri.Host, parsedUri.Port) + sshUrl := fmt.Sprintf("%s:%d", parsedUrl.Host, parsedUrl.Port) sshClient, err := ssh.Dial("tcp", sshUrl, sshClientConfig) if err != nil { - return fmt.Errorf("dial to %s failed: %s", uri, err.Error()) + return fmt.Errorf("dial to %s failed: %s", fileUrl, err.Error()) } defer sshClient.Close() sftpClient, err := sftp.NewClient(sshClient) if err != nil { - return fmt.Errorf("cannot create sftp client to %s: %s", uri, err.Error()) + return fmt.Errorf("cannot create sftp client to %s: %s", fileUrl, err.Error()) } defer sftpClient.Close() - if err := sftpClient.MkdirAll(filepath.Dir(parsedUri.RemotePath)); err != nil { - return fmt.Errorf("cannot create target dir for %s: %s", uri, err.Error()) + if err := sftpClient.MkdirAll(filepath.Dir(parsedUrl.RemotePath)); err != nil { + return fmt.Errorf("cannot create target dir for %s: %s", fileUrl, err.Error()) } srcFile, err := os.Open(srcPath) @@ -128,19 +128,19 @@ func UploadSftpFile(srcPath string, uri string, privateKeys map[string]string) e } defer srcFile.Close() - dstFile, err := sftpClient.Create(parsedUri.RemotePath) + dstFile, err := sftpClient.Create(parsedUrl.RemotePath) if err != nil { - return fmt.Errorf("cannot create on upload %s: %s", uri, err.Error()) + return fmt.Errorf("cannot create on upload %s: %s", fileUrl, err.Error()) } _, err = dstFile.ReadFrom(srcFile) if err != nil { - return fmt.Errorf("cannot read for upload %s to %s : %s", srcPath, uri, err.Error()) + return fmt.Errorf("cannot read for upload %s to %s : %s", srcPath, fileUrl, err.Error()) } err = dstFile.Sync() if err != nil { - return fmt.Errorf("cannot flush for upload %s to %s : %s", srcPath, uri, err.Error()) + return fmt.Errorf("cannot flush for upload %s to %s : %s", srcPath, fileUrl, err.Error()) } return nil diff --git a/test/code/common/util.sh b/test/code/common/util.sh index 9cc1ca50..c8b9432a 100755 --- a/test/code/common/util.sh +++ b/test/code/common/util.sh @@ -233,7 +233,7 @@ one_daemon_run_webapi() fi echo Starting a run in $keyspace at $webapiUrl ... - curl -s -w "\n" -d '{"script_uri":"'$scriptFile'", "script_params_uri":"'$paramsFile'", "start_nodes":"'$startNodes'"}' -H "Content-Type: application/json" -X POST $webapiUrl"/ks/$keyspace/run" + curl -s -w "\n" -d '{"script_url":"'$scriptFile'", "script_params_url":"'$paramsFile'", "start_nodes":"'$startNodes'"}' -H "Content-Type: application/json" -X POST $webapiUrl"/ks/$keyspace/run" if [ "$?" != "0" ]; then exit 1 fi diff --git a/test/code/fannie_mae/README.md b/test/code/fannie_mae/README.md index f9cd04a0..e5abe9a1 100644 --- a/test/code/fannie_mae/README.md +++ b/test/code/fannie_mae/README.md @@ -14,13 +14,11 @@ fannie_mae_bigtest is a variation that uses large number of payment records borr ## Workflow -The [DOT diagram](../../../doc/glossary.md#dot-diagrams) generated with +The diagram generated with ``` -go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/fannie_mae_quicktest/script.json -params_file=../../../test/data/cfg/fannie_mae_bigtest/script_params.json -idx_dag=true +go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/fannie_mae_quicktest/script.json -params_file=../../../test/data/cfg/fannie_mae_quicktest/script_params.json -detail=idx ``` -and rendered in https://dreampuf.github.io/GraphvizOnline : - -![drawing](../../../doc/dot-fanniemae.svg) +![drawing](../../../doc/viz-fanniemae.svg) Full transcript of what the result of each script node looks like in Cassandra - [transcript_fannie_mae.md](../../../doc/transcript_fannie_mae.md). diff --git a/test/code/lookup/README.md b/test/code/lookup/README.md index 01f40618..6170a136 100644 --- a/test/code/lookup/README.md +++ b/test/code/lookup/README.md @@ -15,13 +15,12 @@ and requires test data to be generated - see [1_create_data.sh](./bigtest_cloud/ ## Workflow -The [DOT diagram](../../../doc/glossary.md#dot-diagrams) generated with +The diagram generated with ``` -go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/lookup_quicktest/script.json -params_file=../../../test/data/cfg/lookup_quicktest/script_params_two_runs.json -idx_dag=true +go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/lookup_quicktest/script.json -params_file=../../../test/data/cfg/lookup_quicktest/script_params_two_runs.json -detail=idx ``` -and rendered in https://dreampuf.github.io/GraphvizOnline : -![drawing](../../../doc/dot-lookup.svg) +![drawing](../../../doc/viz-lookup.svg) ## What's tested: diff --git a/test/code/lookup/quicktest_s3/1_create_data.sh b/test/code/lookup/quicktest_s3/1_create_data.sh index ee097b2b..fe325ebf 100755 --- a/test/code/lookup/quicktest_s3/1_create_data.sh +++ b/test/code/lookup/quicktest_s3/1_create_data.sh @@ -4,7 +4,7 @@ source ../../common/util.sh check_s3 # Generate in and out-baseline files -../quicktest/1_create_data.sh +../quicktest_local_fs/1_create_data.sh cfgS3=s3://$CAPILLARIES_AWS_TESTBUCKET/capi_cfg/lookup_quicktest inS3=s3://$CAPILLARIES_AWS_TESTBUCKET/capi_in/lookup_quicktest diff --git a/test/code/portfolio/README.md b/test/code/portfolio/README.md index d320460c..5f9d209c 100644 --- a/test/code/portfolio/README.md +++ b/test/code/portfolio/README.md @@ -15,13 +15,12 @@ and requires test data to be generated - see [1_create_data.sh](./bigtest_cloud/ ## Workflow -The [DOT diagram](../../../doc/glossary.md#dot-diagrams) generated with +The diagram generated with ``` -go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/portfolio_quicktest/script.json -params_file=../../../test/data/cfg/portfolio_quicktest/script_params.json -idx_dag=true +go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/portfolio_quicktest/script.json -params_file=../../../test/data/cfg/portfolio_quicktest/script_params.json -detail=idx ``` -and rendered in https://dreampuf.github.io/GraphvizOnline : -![drawing](./doc/dot-portfolio.svg) +![drawing](./doc/viz-portfolio.svg) Or, here is a bit more low-level walkthrough diff --git a/test/code/portfolio/doc/dot-portfolio.svg b/test/code/portfolio/doc/dot-portfolio.svg deleted file mode 100644 index 06ba1a3a..00000000 --- a/test/code/portfolio/doc/dot-portfolio.svg +++ /dev/null @@ -1,320 +0,0 @@ - - -indexes - - - -/tmp/capi_in/portfolio_quicktest/txns.csv - - -/tmp/capi_in/portfolio_quicktest/txns.csv - - - - - -txns - - -1_read_txns - -creates table: -txns - - - - - -/tmp/capi_in/portfolio_quicktest/txns.csv->txns - - - - - -account_txns - - -2_account_txns_outer - -creates table: -account_txns - -group:true, join:left - - - - - -txns->account_txns - - -idx_txns_account_id (lookup) - - - -accounts - - -1_read_accounts - -creates table: -accounts - - - - - -accounts->account_txns - - -accounts (10 batches) - - - -account_period_holdings - - -2_account_period_holdings_outer - -creates table: -account_period_holdings - -group:true, join:left - - - - - -accounts->account_period_holdings - - -accounts (10 batches) - - - -account_period_activity - - -3_build_account_period_activity - -creates table: -account_period_activity - -group:false, join:left - - - - - -account_txns->account_period_activity - - -account_txns (10 batches) - - - -/tmp/capi_in/portfolio_quicktest/holdings.csv - - -/tmp/capi_in/portfolio_quicktest/holdings.csv - - - - - -period_holdings - - -1_read_period_holdings - -creates table: -period_holdings - - - - - -/tmp/capi_in/portfolio_quicktest/holdings.csv->period_holdings - - - - - -period_holdings->account_period_holdings - - -idx_period_holdings_account_id (lookup) - - - -account_period_holdings->account_period_activity - - -idx_account_period_holdings_account_id (lookup) - - - -account_period_perf - - -4_calc_account_period_perf - -creates table: -account_period_perf - - - - - -account_period_activity->account_period_perf - - -account_period_activity (10 batches) - - - -account_period_perf_by_period - - -5_tag_by_period - -creates table: -account_period_perf_by_period - - - - - -account_period_perf->account_period_perf_by_period - - -account_period_perf (10 batches) - - - -account_period_perf_by_period_sector - - -5_tag_by_sector - -creates table: -account_period_perf_by_period_sector - - - - - -account_period_perf_by_period->account_period_perf_by_period_sector - - -account_period_perf_by_period (10 batches) - - - -account_period_sector_twr_cagr - - -6_perf_json_to_columns - -creates table: -account_period_sector_twr_cagr - - - - - -account_period_perf_by_period_sector->account_period_sector_twr_cagr - - -account_period_perf_by_period_sector (100 batches) - - - -/tmp/capi_in/portfolio_quicktest/accounts.csv - - -/tmp/capi_in/portfolio_quicktest/accounts.csv - - - - - -/tmp/capi_in/portfolio_quicktest/accounts.csv->accounts - - - - - -7_file_account_period_sector_perf - - -7_file_account_period_sector_perf - -creates file: -/tmp/capi_out/portfolio_quicktest/account_period_sector_perf.csv - - - - - -account_period_sector_twr_cagr->7_file_account_period_sector_perf - - -account_period_sector_twr_cagr (no parallelism) - - - -7_file_account_year_perf - - -7_file_account_year_perf - -creates file: -/tmp/capi_out/portfolio_quicktest/account_year_perf.csv - - - - - -account_period_sector_twr_cagr->7_file_account_year_perf - - -account_period_sector_twr_cagr (no parallelism) - - - -/tmp/capi_out/portfolio_quicktest/account_period_sector_perf.csv - - - - -/tmp/capi_out/portfolio_quicktest/account_period_sector_perf.csv - - - - - -7_file_account_period_sector_perf->/tmp/capi_out/portfolio_quicktest/account_period_sector_perf.csv - - - - - -/tmp/capi_out/portfolio_quicktest/account_year_perf.csv - - - - -/tmp/capi_out/portfolio_quicktest/account_year_perf.csv - - - - - -7_file_account_year_perf->/tmp/capi_out/portfolio_quicktest/account_year_perf.csv - - - - - \ No newline at end of file diff --git a/test/code/portfolio/doc/viz-portfolio.svg b/test/code/portfolio/doc/viz-portfolio.svg new file mode 100644 index 00000000..f9c02472 --- /dev/null +++ b/test/code/portfolio/doc/viz-portfolio.svg @@ -0,0 +1,443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1_read_period_holdings + Load holdings from csv + Processor: read from files into a table + File(s): /tmp/capi_in/.../holdings.csv + Table created: period_holdings + + + + + + + + 1_read_accounts + Load accounts from csv + Processor: read from files into a table + File(s): /tmp/capi_in/.../accounts.csv + Table created: accounts + + + + + + + + 2_account_period_holdings_outer + For each account, merge all holdings into single json string + Processor: left join with lookup table, group: true + Lookup index: idx_period_holdings_account_id + Table created: account_period_holdings + + + + + accounts + (10 batches) + + + + + idx_period_holdings_account_id + (lookup) + + + + + + + + 2_account_txns_outer + For each account, merge all txns into single json string + Processor: left join with lookup table, group: true + Lookup index: idx_txns_account_id + Table created: account_txns + + + + + accounts + (10 batches) + + + + + idx_txns_account_id + (lookup) + + + + + + + + 3_build_account_period_activity + For each account, merge holdings and txns + Processor: left join with lookup table, group: false + Lookup index: idx_account_period_holdings_account_id + Table created: account_period_activity + + + + + account_txns + (10 batches) + + + + + idx_account_period_holdings_account_id + (lookup) + + + + + + + + 4_calc_account_period_perf + Apply Python-based calculations to account holdings and txns + Processor: apply Python calculations + Python file(s): /tmp/capi_cfg/.../portfolio_test_company_info_provider.py + Table created: account_period_perf + + + + + account_period_activity + (10 batches) + + + + + + + + 5_tag_by_period + Tag accounts by period name + Processor: tag and denormalize + Tag criteria URL: (inline) + Table created: account_period_perf_by_period + + + + + account_period_perf + (10 batches) + + + + + + + + 5_tag_by_sector + Tag accounts by sector + Processor: tag and denormalize + Tag criteria URL: (inline) + Table created: account_period_perf_by_period_sector + + + + + account_period_perf_by_period + (10 batches) + + + + + + + + 6_perf_json_to_columns + Use Python to read perf json and save stats as columns + Processor: apply Python calculations + Python file(s): /tmp/capi_cfg/.../json_to_columns.py + Table created: account_period_sector_twr_cagr + + + + + account_period_perf_by_period_sector + (100 batches) + + + + + + + + 7_file_account_period_sector_perf + Write yearly/quarterly perf results by sector to CSV file + Processor: read from table into files + Source table: account_period_sector_twr_cagr + File(s): /tmp/capi_out/.../account_period_sector_perf.csv + + + + + account_period_sector_twr_cagr + (no parallelism) + + + + + + + + 7_file_account_year_perf + Write yearly perf results for all sectors to CSV file + Processor: read from table into files + Source table: account_period_sector_twr_cagr + File(s): /tmp/capi_out/.../account_year_perf.csv + + + + + account_period_sector_twr_cagr + (no parallelism) + + + + + + + + 1_read_txns + Load txns from csv + Processor: read from files into a table + File(s): /tmp/capi_in/.../txns.csv + Table created: txns + +Perms 24, elapsed 0.000s, dist 2864.6 + diff --git a/test/code/proto_file_reader_creator/README.md b/test/code/proto_file_reader_creator/README.md index 49678981..62a19cd3 100644 --- a/test/code/proto_file_reader_creator/README.md +++ b/test/code/proto_file_reader_creator/README.md @@ -4,14 +4,13 @@ Created using Ubuntu WSL. Other Linux flavors and MacOS may require edits. ## Workflow -There are no Capillaries scripts for this test in the Capillaries codebase. The scripts are generated on the fly by [1_generate_scripts.sh](./1_generate_scripts.sh) and saved to `/tmp/capi_cfg/proto_file_reader_creator_quicktest`. After the scripts are generated, you can generate the [DOT diagram](../../../doc/glossary.md#dot-diagrams) diagram using this command: +There are no Capillaries scripts for this test in the Capillaries codebase. The scripts are generated on the fly by [1_generate_scripts.sh](./1_generate_scripts.sh) and saved to `/tmp/capi_cfg/proto_file_reader_creator_quicktest`. After the scripts are generated, you can generate the diagram using this command: ``` -go run capitoolbelt.go validate_script -script_file=/tmp/capi_cfg/proto_file_reader_creator_quicktest/script_csv.json -idx_dag=true +go run capitoolbelt.go validate_script -script_file=/tmp/capi_cfg/proto_file_reader_creator_quicktest/script_csv.json -detail=idx ``` -and render it in https://dreampuf.github.io/GraphvizOnline : -![drawing](../../../doc/dot-proto-file-reader-creator.svg) +![drawing](../../../doc/viz-proto-file-reader-creator.svg) ## What's tested: diff --git a/test/code/py_calc/README.md b/test/code/py_calc/README.md index 0b280543..8a1c9411 100644 --- a/test/code/py_calc/README.md +++ b/test/code/py_calc/README.md @@ -8,13 +8,12 @@ User-supplied formulas are expected to reside in test/data/cfg/py_calc/py direct ## Workflow -The [DOT diagram](../../../doc/glossary.md#dot-diagrams) generated with +The diagram generated with ``` -go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/py_calc/script.json -params_file=../../../test/data/cfg/py_calc/script_params.json -idx_dag=true +go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/py_calc_quicktest/script.json -params_file=../../../test/data/cfg/py_calc_quicktest/script_params.json -detail=idx ``` -and rendered in https://dreampuf.github.io/GraphvizOnline : -![drawing](../../../doc/dot-pycalc.svg) +![drawing](../../../doc/viz-pycalc.svg) ## What's tested: diff --git a/test/code/tag_and_denormalize/README.md b/test/code/tag_and_denormalize/README.md index eb96bd54..bdd2526f 100644 --- a/test/code/tag_and_denormalize/README.md +++ b/test/code/tag_and_denormalize/README.md @@ -4,13 +4,12 @@ Created using Ubuntu WSL. Other Linux flavors and MacOS may require edits. ## Workflow -The [DOT diagram](../../../doc/glossary.md#dot-diagrams) generated with +The diagram generated with ``` -go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/tag_and_denormalize_quicktest/script.json -params_file=../../../test/data/cfg/tag_and_denormalize/script_params_two_runs.json -idx_dag=true +go run capitoolbelt.go validate_script -script_file=../../../test/data/cfg/tag_and_denormalize_quicktest/script.json -params_file=../../../test/data/cfg/tag_and_denormalize_quicktest/script_params_two_runs.json -detail=idx ``` -and rendered in https://dreampuf.github.io/GraphvizOnline : -![drawing](../../../doc/dot-tag-and-denormalize.svg) +![drawing](../../../doc/viz-tag-and-denormalize.svg) ## What's tested: diff --git a/test/data/cfg/tag_and_denormalize_quicktest/script.json b/test/data/cfg/tag_and_denormalize_quicktest/script.json index eb8f4a05..5da1193f 100644 --- a/test/data/cfg/tag_and_denormalize_quicktest/script.json +++ b/test/data/cfg/tag_and_denormalize_quicktest/script.json @@ -5,7 +5,7 @@ "desc": "Load tags from file", "r": { "urls": [ - "{dir_in}/tags.csv{uri_suffix}" + "{dir_in}/tags.csv{url_suffix}" ], "csv": { "first_data_line_idx": 0 @@ -35,7 +35,7 @@ "explicit_run_only": true, "r": { "urls": [ - "{dir_in}/flipcart_products.tsv{uri_suffix}" + "{dir_in}/flipcart_products.tsv{url_suffix}" ], "csv": { "separator": "\t", @@ -113,7 +113,7 @@ }, "p": { "tag_field_name": "tag", - "tag_criteria_uri": "{dir_cfg}/tag_criteria.json{uri_suffix}" + "tag_criteria_url": "{dir_cfg}/tag_criteria.json{url_suffix}" }, "w": { "name": "tagged_products", @@ -181,7 +181,7 @@ "tag_totals": { "type": "table_lookup_table", "start_policy": "{totals_start_policy|string}", - "desc": "For each tag, look up produts and perform left outer join with grouping and aggregation", + "desc": "For each tag, look up products and perform left outer join with grouping and aggregation", "r": { "table": "tags", "expected_batches_total": 10 diff --git a/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run.json b/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run.json index e5a1bb97..948bb90c 100644 --- a/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run.json +++ b/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run.json @@ -1,6 +1,6 @@ { "totals_start_policy": "auto", - "uri_suffix": "", + "url_suffix": "", "dir_cfg": "/tmp/capi_cfg/tag_and_denormalize_quicktest", "dir_in": "/tmp/capi_in/tag_and_denormalize_quicktest", "dir_out": "/tmp/capi_out/tag_and_denormalize_quicktest" diff --git a/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run_input_https.json b/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run_input_https.json index 86524d58..55de43fe 100644 --- a/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run_input_https.json +++ b/test/data/cfg/tag_and_denormalize_quicktest/script_params_one_run_input_https.json @@ -1,6 +1,6 @@ { "totals_start_policy": "auto", - "uri_suffix": "", + "url_suffix": "", "dir_cfg": "https://raw.githubusercontent.com/capillariesio/capillaries/main/test/data/cfg/tag_and_denormalize_quicktest", "dir_in": "https://raw.githubusercontent.com/capillariesio/capillaries/main/test/data/in/tag_and_denormalize_quicktest", "dir_out": "/tmp/capi_out/tag_and_denormalize_quicktest" diff --git a/test/data/cfg/tag_and_denormalize_quicktest/script_params_two_runs.json b/test/data/cfg/tag_and_denormalize_quicktest/script_params_two_runs.json index 54ece208..3cf78ecf 100644 --- a/test/data/cfg/tag_and_denormalize_quicktest/script_params_two_runs.json +++ b/test/data/cfg/tag_and_denormalize_quicktest/script_params_two_runs.json @@ -1,6 +1,6 @@ { "totals_start_policy": "manual", - "uri_suffix": "", + "url_suffix": "", "dir_cfg": "/tmp/capi_cfg/tag_and_denormalize_quicktest", "dir_in": "/tmp/capi_in/tag_and_denormalize_quicktest", "dir_out": "/tmp/capi_out/tag_and_denormalize_quicktest" diff --git a/test/docker/cassandra/Dockerfile b/test/docker/cassandra/Dockerfile index 98713dc1..5dcc3801 100755 --- a/test/docker/cassandra/Dockerfile +++ b/test/docker/cassandra/Dockerfile @@ -1,6 +1,6 @@ -FROM cassandra:4.1 +FROM cassandra:5.0.2 -ARG JMX_EXPORTER_VERSION=0.20.0 +ARG JMX_EXPORTER_VERSION=1.0.1 RUN mkdir /prometheus ADD "https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/$JMX_EXPORTER_VERSION/jmx_prometheus_javaagent-$JMX_EXPORTER_VERSION.jar" /prometheus diff --git a/test/docker/fluentd/Dockerfile b/test/docker/fluentd/Dockerfile index 81d839ba..222ef783 100644 --- a/test/docker/fluentd/Dockerfile +++ b/test/docker/fluentd/Dockerfile @@ -1,3 +1,3 @@ -FROM fluent/fluentd:v1.17-1 +FROM fluent/fluentd:v1.18-1 RUN rm /fluentd/etc/fluent.conf ADD ./test/docker/fluentd/fluent.conf /fluentd/etc \ No newline at end of file diff --git a/test/docker/fluentd/fluent.conf b/test/docker/fluentd/fluent.conf index e244dc51..25748110 100755 --- a/test/docker/fluentd/fluent.conf +++ b/test/docker/fluentd/fluent.conf @@ -26,7 +26,7 @@ @type file path /fluentd/log/webapi.*.log - time_key fluentd_time # Add ts - we mau want to know when the msg hit fluentd + time_key fluentd_time # Add ts - we may want to know when the msg hit fluentd time_type string time_format %Y-%m-%dT%H:%M:%S.%NZ @@ -40,7 +40,7 @@ @type file path /fluentd/log/daemon.*.log - time_key fluentd_time # Add ts - we mau want to know when the msg hit fluentd + time_key fluentd_time # Add ts - we may want to know when the msg hit fluentd time_type string time_format %Y-%m-%dT%H:%M:%S.%NZ @@ -54,7 +54,7 @@ @type file path /fluentd/log/cassandra.*.log - time_key fluentd_time # Add ts - we mau want to know when the msg hit fluentd + time_key fluentd_time # Add ts - we may want to know when the msg hit fluentd time_type string time_format %Y-%m-%dT%H:%M:%S.%NZ @@ -68,7 +68,7 @@ @type file path /fluentd/log/rabbitmq.*.log - time_key fluentd_time # Add ts - we mau want to know when the msg hit fluentd + time_key fluentd_time # Add ts - we may want to know when the msg hit fluentd time_type string time_format %Y-%m-%dT%H:%M:%S.%NZ @@ -82,7 +82,7 @@ @type file path /fluentd/log/prometheus.*.log - time_key fluentd_time # Add ts - we mau want to know when the msg hit fluentd + time_key fluentd_time # Add ts - we may want to know when the msg hit fluentd time_type string time_format %Y-%m-%dT%H:%M:%S.%NZ @@ -96,7 +96,7 @@ @type file path /fluentd/log/ui.*.log - time_key fluentd_time # Add ts - we mau want to know when the msg hit fluentd + time_key fluentd_time # Add ts - we may want to know when the msg hit fluentd time_type string time_format %Y-%m-%dT%H:%M:%S.%NZ diff --git a/test/docker/prometheus/prometheus.yml b/test/docker/prometheus/prometheus.yml index 06928c99..f0d5ad65 100755 --- a/test/docker/prometheus/prometheus.yml +++ b/test/docker/prometheus/prometheus.yml @@ -8,4 +8,4 @@ scrape_configs: - job_name: 'node_exporter' scrape_interval: 5s static_configs: - - targets: ['10.5.0.11:7070','10.5.0.12:7070'] + - targets: ['10.5.0.11:7070'] #['10.5.0.11:7070','10.5.0.12:7070'] diff --git a/test/k8s/README.md b/test/k8s/README.md index 64a5fb77..8adb0d1c 100755 --- a/test/k8s/README.md +++ b/test/k8s/README.md @@ -2,7 +2,7 @@ This is a POC demonstrating Capillaries running in a Kubernetes cluster. It assumes that: - S3 docker repositories are set up - - S3 bucket that hold lookup_quicktest_s3 Capillaries script and input data files are provisioned + - S3 bucket that holds lookup_quicktest_s3 Capillaries script and input data files are provisioned - Capillaries docker images were built and uploaded using binaries_build.sh, images_build.sh, images_upload.sh commands - Minikube Kuberetes cluster is running diff --git a/ui/.eslintignore b/ui/.eslintignore deleted file mode 100644 index a6ca7295..00000000 --- a/ui/.eslintignore +++ /dev/null @@ -1,12 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock diff --git a/ui/docker/Dockerfile b/ui/docker/Dockerfile index 7232a871..d67c534a 100755 --- a/ui/docker/Dockerfile +++ b/ui/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:21.7.0-alpine +FROM node:23.3.0-alpine WORKDIR /usr/src/capillaries diff --git a/ui/eslint.config.js b/ui/eslint.config.js new file mode 100644 index 00000000..ce3c1f81 --- /dev/null +++ b/ui/eslint.config.js @@ -0,0 +1,38 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import { includeIgnoreFile } from '@eslint/compat'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...svelte.configs['flat/recommended'], + prettier, + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + // Note: there should be no other properties in this object + ignores: [ + '**/temp.js', + 'config/*', + '.DS_Store', + 'node_modules', + '/build', + '/.svelte-kit', + 'package', + '.env.*', + '!.env.example' + ] + } +]; diff --git a/ui/package-lock.json b/ui/package-lock.json index a801f888..e489307b 100755 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,19 +8,22 @@ "name": "capillaries-ui", "version": "1.0.0", "devDependencies": { - "@sveltejs/adapter-auto": "^3.2.5", - "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/kit": "^2.5.28", + "@eslint/compat": "^1.2.4", + "@sveltejs/adapter-auto": "^3.3.1", + "@sveltejs/adapter-static": "^3.0.6", + "@sveltejs/kit": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^5.0.1", "dayjs": "^1.11.13", - "eslint": "^9.11.0", + "eslint": "^9.16.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.44.0", - "prettier": "^3.3.3", - "prettier-plugin-svelte": "^3.2.6", - "svelte": "^4.2.19", - "svelte-modals": "^1.3.0", + "eslint-plugin-svelte": "^2.46.1", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.2.7", + "rollup-plugin-visualizer": "^5.12.0", + "svelte": "^5.9.0", + "svelte-modals": "^2.0.0", "urlpattern-polyfill": "^10.0.0", - "vite": "^5.4.7", + "vite": "^6.0.3", "vite-plugin-filter-replace": "^0.1.13" } }, @@ -38,371 +41,411 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -421,21 +464,41 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/compat": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz", + "integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -443,11 +506,25 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/core": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -467,28 +544,31 @@ } }, "node_modules/@eslint/js": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.0.tgz", - "integrity": "sha512-LPkkenkDqyzTFauZLLAPhIb48fj6drrfMvRGSL9tS3AcZBSVTllemLSNyCvHNNL2t797S/6DJNSIwRwXgMO/eQ==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "levn": "^0.4.1" }, @@ -496,6 +576,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -510,10 +628,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -555,10 +674,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -570,260 +690,285 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@polka/url": { - "version": "1.0.0-next.25", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", - "dev": true + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", - "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", - "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", - "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", - "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", - "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", - "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", - "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", - "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", - "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", - "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", - "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", - "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", - "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", - "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", - "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", - "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@sveltejs/adapter-auto": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.5.tgz", - "integrity": "sha512-27LR+uKccZ62lgq4N/hvyU2G+hTP9fxWEAfnZcl70HnyfAjMSsGk1z/SjAPXNCD1mVJIE7IFu3TQ8cQ/UH3c0A==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", + "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", "dev": true, + "license": "MIT", "dependencies": { "import-meta-resolve": "^4.1.0" }, @@ -832,32 +977,34 @@ } }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.5.tgz", - "integrity": "sha512-kFJR7RxeB6FBvrKZWAEzIALatgy11ISaaZbcPup8JdWUdrmmfUHHTJ738YHJTEfnCiiXi6aX8Q6ePY7tnSMD6Q==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.6.tgz", + "integrity": "sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==", "dev": true, + "license": "MIT", "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "node_modules/@sveltejs/kit": { - "version": "2.5.28", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.28.tgz", - "integrity": "sha512-/O7pvFGBsQPcFa9UrW8eUC5uHTOXLsUp3SN0dY6YmRAL9nfPSrJsSJk//j5vMpinSshzUjteAFcfQTU+04Ka1w==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.9.0.tgz", + "integrity": "sha512-W3E7ed3ChB6kPqRs2H7tcHp+Z7oiTFC6m+lLyAQQuyXeqw6LdNuuwEUla+5VM0OGgqQD+cYD6+7Xq80vVm17Vg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", - "devalue": "^5.0.0", - "esm-env": "^1.0.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.1", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", - "sirv": "^2.0.4", + "sirv": "^3.0.0", "tiny-glob": "^0.2.9" }, "bin": { @@ -867,50 +1014,49 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" + "vite": "^5.0.3 || ^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.0.tgz", - "integrity": "sha512-sY6ncCvg+O3njnzbZexcVtUqOBE3iYmQPJ9y+yXSkOwG576QI/xJrBnQSRXFLGwJNBa0T78JEKg5cIR0WOAuUw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.1.tgz", + "integrity": "sha512-D5l5+STmywGoLST07T9mrqqFFU+xgv5fqyTWM+VbxTvQ6jujNn4h3lQNCvlwVYs4Erov8i0K5Rwr3LQtmBYmBw==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", - "debug": "^4.3.4", + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.0", + "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", - "magic-string": "^0.30.9", - "svelte-hmr": "^0.16.0", - "vitefu": "^0.2.5" + "magic-string": "^0.30.13", + "vitefu": "^1.0.3" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", - "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "debug": "^4.3.4" + "debug": "^4.3.7" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, "node_modules/@types/cookie": { @@ -920,16 +1066,25 @@ "dev": true }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -942,15 +1097,27 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-typescript": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", + "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": ">=8.9.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -967,6 +1134,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -990,37 +1158,42 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1031,6 +1204,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1051,17 +1225,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/color-convert": { @@ -1086,7 +1262,8 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cookie": { "version": "0.6.0", @@ -1098,10 +1275,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1111,19 +1289,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1143,12 +1308,13 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1170,62 +1336,82 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/devalue": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz", - "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==", - "dev": true + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/escape-string-regexp": { @@ -1241,28 +1427,32 @@ } }, "node_modules/eslint": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.0.tgz", - "integrity": "sha512-yVS6XODx+tMFMDFcG4+Hlh+qG7RM6cCJXtQhCKLSsr3XkLvWggHjCqjfh0XsPPnt1c56oaT6PMgW9XWQQjdHXA==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.11.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.16.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.5", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1272,14 +1462,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -1327,22 +1514,23 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.44.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.44.0.tgz", - "integrity": "sha512-wav4MOs02vBb1WjvTCYItwJCxMkuk2Z4p+K/eyjL0N/z7ahXLP+0LtQQjiKc2ezuif7GnZLbD1F3o1VHzSvdVg==", + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@jridgewell/sourcemap-codec": "^1.4.15", "eslint-compat-utils": "^0.5.1", "esutils": "^2.0.3", - "known-css-properties": "^0.34.0", + "known-css-properties": "^0.35.0", "postcss": "^8.4.38", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", - "svelte-eslint-parser": "^0.41.1" + "svelte-eslint-parser": "^0.43.0" }, "engines": { "node": "^14.17.0 || >=16.0.0" @@ -1352,7 +1540,7 @@ }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191" + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { "svelte": { @@ -1361,10 +1549,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -1389,10 +1578,11 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1401,20 +1591,22 @@ } }, "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", - "dev": true + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", + "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", + "dev": true, + "license": "MIT" }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1424,10 +1616,11 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1447,11 +1640,23 @@ "node": ">=0.10" } }, + "node_modules/esrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.3.tgz", + "integrity": "sha512-ZlQmCCK+n7SGoqo7DnfKaP1sJZa49P01/dXzmjCASSo04p72w8EksT2NMK8CEX8DhKsfJXANioIw8VyHNsBfvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1468,15 +1673,6 @@ "node": ">=4.0" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1490,13 +1686,15 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -1504,15 +1702,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1566,6 +1755,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1574,6 +1764,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1591,6 +1791,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1624,6 +1825,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -1633,6 +1835,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1663,6 +1866,22 @@ "node": ">=0.8.19" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1672,6 +1891,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1684,35 +1913,42 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-inside": { + "node_modules/is-reference": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" } }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "*" + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1730,7 +1966,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -1757,10 +1994,11 @@ } }, "node_modules/known-css-properties": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", - "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", - "dev": true + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" }, "node_modules/levn": { "version": "0.4.1", @@ -1812,25 +2050,21 @@ "dev": true }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1852,15 +2086,17 @@ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.7", @@ -1886,6 +2122,24 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1938,6 +2192,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -1959,31 +2214,35 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } + "license": "ISC" }, - "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -1999,9 +2258,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -2037,6 +2297,16 @@ } } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", @@ -2072,6 +2342,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "engines": { "node": ">=12.0" }, @@ -2102,10 +2373,11 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -2117,10 +2389,11 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.6.tgz", - "integrity": "sha512-Y1XWLw7vXUQQZmgv1JAEiLcErqUniAF2wO7QJsw8BVMvpLET2dI5WpEIEJx1r11iHVdSMzQxivyfrH9On9t2IQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.2.tgz", + "integrity": "sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==", "dev": true, + "license": "MIT", "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" @@ -2131,56 +2404,39 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", - "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -2190,46 +2446,53 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.4", - "@rollup/rollup-android-arm64": "4.22.4", - "@rollup/rollup-darwin-arm64": "4.22.4", - "@rollup/rollup-darwin-x64": "4.22.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", - "@rollup/rollup-linux-arm-musleabihf": "4.22.4", - "@rollup/rollup-linux-arm64-gnu": "4.22.4", - "@rollup/rollup-linux-arm64-musl": "4.22.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", - "@rollup/rollup-linux-riscv64-gnu": "4.22.4", - "@rollup/rollup-linux-s390x-gnu": "4.22.4", - "@rollup/rollup-linux-x64-gnu": "4.22.4", - "@rollup/rollup-linux-x64-musl": "4.22.4", - "@rollup/rollup-win32-arm64-msvc": "4.22.4", - "@rollup/rollup-win32-ia32-msvc": "4.22.4", - "@rollup/rollup-win32-x64-msvc": "4.22.4", + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "license": "MIT", "dependencies": { - "queue-microtask": "^1.2.2" + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, "node_modules/sade": { @@ -2267,6 +2530,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2279,22 +2543,34 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", "dev": true, + "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" } }, "node_modules/source-map-js": { @@ -2306,11 +2582,27 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2323,6 +2615,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2343,35 +2636,36 @@ } }, "node_modules/svelte": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", - "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/estree": "^1.0.1", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^4.0.0", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.9.0.tgz", + "integrity": "sha512-ZcC3BtjIDa4yfhAyAr94MxDQLD97zbpXmaUldFv2F5AkdZwYgQYB3BZVNRU5zEVaeeHoAns8ADiRMnre3QmpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "acorn-typescript": "^1.4.13", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "esm-env": "^1.2.1", + "esrap": "^1.2.3", + "is-reference": "^3.0.3", "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/svelte-eslint-parser": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.1.tgz", - "integrity": "sha512-08ndI6zTghzI8SuJAFpvMbA/haPSGn3xz19pjre19yYMw8Nw/wQJ2PrZBI/L8ijGTgtkWCQQiLLy+Z1tfaCwNA==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", "dev": true, + "license": "MIT", "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", @@ -2386,7 +2680,7 @@ "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191" + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { "svelte": { @@ -2399,6 +2693,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2415,6 +2710,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -2427,34 +2723,16 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/svelte-hmr": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", - "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, "node_modules/svelte-modals": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/svelte-modals/-/svelte-modals-1.3.0.tgz", - "integrity": "sha512-b1Ylnyv9O6b7VYeWGJVToaVU2lw7GtErVwiEdojyfnOuZcrhNlQ5eDqbTrL3xyKz8j2VTy/QiGUl1lm/6SnQ2A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svelte-modals/-/svelte-modals-2.0.0.tgz", + "integrity": "sha512-wBClXmScNStF/rG75ZP7hEiKXRJ5H+DhA2dkMPQd+FX2Hc1XTj1n5cZGC7uTYCYOVmQwrsDOlL5a2V5E6q3f+A==", "dev": true, + "license": "MIT", "peerDependencies": { - "svelte": "^3.0.0 || ^4.0.0" + "svelte": ">=5" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -2470,6 +2748,7 @@ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2491,6 +2770,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -2508,20 +2788,21 @@ "dev": true }, "node_modules/vite": { - "version": "5.4.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", - "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", + "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.24.0", + "postcss": "^8.4.49", + "rollup": "^4.23.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2530,19 +2811,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -2563,6 +2850,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -2573,13 +2866,17 @@ "dev": true }, "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.4.tgz", + "integrity": "sha512-y6zEE3PQf6uu/Mt6DTJ9ih+kyJLr4XcSgHR2zUkM8SWDhuixEJxfJ6CZGMHh1Ec3vPLoEA0IHU5oWzVqw8ulow==", "dev": true, - "peer": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { "vite": { @@ -2592,6 +2889,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2611,13 +2909,76 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/yocto-queue": { @@ -2631,6 +2992,13 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, + "license": "MIT" } } } diff --git a/ui/package.json b/ui/package.json index 3a30e6c5..f7fc92a2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,19 +10,22 @@ "format": "prettier --write ." }, "devDependencies": { - "@sveltejs/adapter-auto": "^3.2.5", - "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/kit": "^2.5.28", + "@eslint/compat": "^1.2.4", + "@sveltejs/adapter-auto": "^3.3.1", + "@sveltejs/adapter-static": "^3.0.6", + "@sveltejs/kit": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^5.0.1", "dayjs": "^1.11.13", - "eslint": "^9.11.0", + "eslint": "^9.16.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.44.0", - "prettier": "^3.3.3", - "prettier-plugin-svelte": "^3.2.6", - "svelte": "^4.2.19", - "svelte-modals": "^1.3.0", + "eslint-plugin-svelte": "^2.46.1", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.2.7", + "rollup-plugin-visualizer": "^5.12.0", + "svelte": "^5.9.0", + "svelte-modals": "^2.0.0", "urlpattern-polyfill": "^10.0.0", - "vite": "^5.4.7", + "vite": "^6.0.3", "vite-plugin-filter-replace": "^0.1.13" }, "type": "module" diff --git a/ui/src/Tabs.svelte b/ui/src/Tabs.svelte new file mode 100644 index 00000000..268d7d35 --- /dev/null +++ b/ui/src/Tabs.svelte @@ -0,0 +1,59 @@ + + +
    + {#each items as item} +
  • + {item.label} +
  • + {/each} +
+ {#each items as item} + {#if activeTabValue == item.value} +
+ +
+ {/if} + {/each} + \ No newline at end of file diff --git a/ui/src/Util.svelte b/ui/src/Util.svelte index 136158cb..bb15deed 100644 --- a/ui/src/Util.svelte +++ b/ui/src/Util.svelte @@ -1,11 +1,17 @@ - - diff --git a/ui/src/modals/ModalStartRun.svelte b/ui/src/modals/ModalStartRun.svelte index 8fd55693..97cc449a 100644 --- a/ui/src/modals/ModalStartRun.svelte +++ b/ui/src/modals/ModalStartRun.svelte @@ -1,45 +1,42 @@ - - - - - - - - - - - - -
Node batch execution summary
- - {@html svgSummary} - - {#if executionDetailsVisible} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Elapsed:{nodeElapsed}s(not very useful, skewed by other nodes)
Elapsed average:{elapsedAverage}s
Elapsed median:{elapsedMedian}s
Batches not started:{nonStartedBatchesPercent.toFixed(0)}%{nonStartedBatchesRatio}{nonStartedBatches}
Batches started:{startedBatchesPercent.toFixed(0)}%{startedBatchesRatio}{startedBatches}
Batches running:{runningBatchesPercent.toFixed(0)}%{runningBatchesRatio}{runningBatches}
Batches finished:{finishedBatchesPercent.toFixed(0)}%{finishedBatchesRatio}{finishedBatches}
- {/if} -
- - - - - - - - - - - - - - {#each batch_history as e} - - - - - - - - - - - - {/each} - -
TimestampBatchStatusElapsedFirst tokenLast tokenHost/InstanceThreadComment
{dayjs(e.ts).format('MMM D, YYYY HH:mm:ss.SSS Z')}{e.batch_idx} / {e.batches_total}{#if e.elapsed > 0} - {e.elapsed} - {/if}{e.first_token}{e.last_token}{e.instance}{e.thread}{e.comment}
- - diff --git a/ui/src/panels/Breadcrumbs.svelte b/ui/src/panels/Breadcrumbs.svelte index 732aa075..02b99f72 100644 --- a/ui/src/panels/Breadcrumbs.svelte +++ b/ui/src/panels/Breadcrumbs.svelte @@ -1,5 +1,5 @@
@@ -9,13 +9,13 @@ alt="" title="Capillaries-UI" /> - {#each pathElements as e, i} + {#each path_elements as e, i} {#if !!e.link} {e.title} {:else} {e.title} {/if} - {#if i < pathElements.length - 1} + {#if i < path_elements.length - 1}   >   {/if} {/each} diff --git a/ui/src/panels/NodeHistory.svelte b/ui/src/panels/NodeHistory.svelte deleted file mode 100644 index 8250619d..00000000 --- a/ui/src/panels/NodeHistory.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - - - -{@html svgSummary} - - - - - - - - - - {#each node_history as e} - - - - - - - - {/each} - -
TimestampNodeStatusElapsedComment
{dayjs(e.ts).format('MMM D, YYYY HH:mm:ss.SSS Z')}{e.script_node}{#if e.elapsed > 0} - {e.elapsed} - {/if}{e.comment}
- - diff --git a/ui/src/panels/RunInfo.svelte b/ui/src/panels/RunInfo.svelte index 0a35bb95..8b392c84 100644 --- a/ui/src/panels/RunInfo.svelte +++ b/ui/src/panels/RunInfo.svelte @@ -1,17 +1,13 @@ - - {#if Object.keys(run_lifespan).length > 0} - - - @@ -74,13 +65,13 @@
Run summary
Run Id:Status   - {util.runStatusToText(run_lifespan.final_status)}  + {runStatusToText(run_lifespan.final_status)}  {#if run_lifespan.final_status != 3}
{/if} - +
+
{#if Object.keys(run_props).length > 0} @@ -100,12 +92,12 @@ - - + + - - + + diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte index 119710e8..c3a2e7b5 100644 --- a/ui/src/routes/+page.svelte +++ b/ui/src/routes/+page.svelte @@ -4,11 +4,10 @@ import Keyspaces from './Keyspaces.svelte'; import KsRunNodeHistory from './KsRunNodeHistory.svelte'; import KsRunNodeBatchHistory from './KsRunNodeBatchHistory.svelte'; - import { Modals, closeModal } from 'svelte-modals'; + import { Modals, modals } from 'svelte-modals'; import { URLPattern } from 'urlpattern-polyfill'; - let params; - let routed_page; + let routed_page, hg; const patternKeyspaces = new URLPattern({ hash: '#/' }); const patternKsMatrix = new URLPattern({ hash: '#/ks/:ks_name/matrix' }); const patternKsRunNodeBatchHistory = new URLPattern({ @@ -24,20 +23,16 @@ function reload() { if (patternKsMatrix.test($page.url)) { - let hg = patternKsMatrix.exec($page.url).hash.groups; - params = { ks_name: hg.ks_name }; + hg = patternKsMatrix.exec($page.url).hash.groups; routed_page = KsMatrix; } else if (patternKsRunNodeHistory.test($page.url)) { - let hg = patternKsRunNodeHistory.exec($page.url).hash.groups; - params = { ks_name: hg.ks_name, run_id: hg.run_id }; + hg = patternKsRunNodeHistory.exec($page.url).hash.groups; routed_page = KsRunNodeHistory; } else if (patternKsRunNodeBatchHistory.test($page.url)) { - let hg = patternKsRunNodeBatchHistory.exec($page.url).hash.groups; - params = { ks_name: hg.ks_name, run_id: hg.run_id, node_name: hg.node_name }; + hg = patternKsRunNodeBatchHistory.exec($page.url).hash.groups; routed_page = KsRunNodeBatchHistory; } else if (patternKeyspaces.test($page.url)) { routed_page = Keyspaces; - params = {}; } else { let url = new URL($page.url); // Navigate to Keyspaces @@ -48,16 +43,24 @@ reload(); - + -
false} - aria-hidden="true" - /> + + {#snippet backdrop()} +
modals.close()} + onkeypress={() => false} + aria-hidden="true" + >
+ {/snippet} diff --git a/ui/src/routes/KsRunNodeHistory.svelte b/ui/src/routes/KsRunNodeHistory.svelte index acc2bf07..7573e649 100644 --- a/ui/src/routes/KsRunNodeHistory.svelte +++ b/ui/src/routes/KsRunNodeHistory.svelte @@ -1,26 +1,112 @@ - - +

{responseError}

- - + +{#snippet tabRunInfo()} + +{/snippet} + +{#snippet tabDiagram()} +

+ This diagram is dynamic and evolves as nodes are processed. Color legend: + node started, + node completed successfully, + node failed, + stop signal received + node not processed as part of this run (maybe yet). + Nodes that require manual start are marked with a thicker border. + To see a static copy of it in a separate window, + click here. + To see detailed script diagram not reflecting run status, + click here for black and white, + or here for colored by root node. +

+ +

{svgError}

+ +
+ + {@html svgStatusViz} +
+{/snippet} + +{#snippet tabHistory()} +
{run_props.run_description}
Script URI:{run_props.script_uri}Script URL:{run_props.script_url}
Script parameters URI:{run_props.script_params_uri}Script parameters URL:{run_props.script_params_url}
Start nodes:
+ + + + + + + + + + + {#each webapiData.node_history as e} + + + + + + + + {/each} + +
TimestampNodeStatusElapsedComment
{dayjs(e.ts).format('MMM D, YYYY HH:mm:ss.SSS Z')}{e.script_node}{#if e.elapsed > 0} + {e.elapsed} + {/if}{e.comment}
+{/snippet} + + + + diff --git a/ui/vite.config.js b/ui/vite.config.js index 03aafb05..670b8d35 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -1,5 +1,6 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; +import { visualizer } from 'rollup-plugin-visualizer'; console.log('In .env file VITE_WEBAPI_URL=$CAPI_WEBAPI_URL:', process.env.CAPI_WEBAPI_URL); export default defineConfig({ @@ -7,11 +8,20 @@ export default defineConfig({ port: 8080, host: true // In Docker containers, "--host 0.0.0.0" may not work. So, tell Vite to bind to 0.0.0.0 here. }, - plugins: [sveltekit()], + plugins: [ + sveltekit(), + visualizer({ + emitFile: true, + filename: 'stats.html' + }) + ], build: { - minify: true - }, - esbuild: { - minifyIdentifiers: true + rollupOptions: { + output: { + manualChunks: (id) => { + return 'capillaries-ui'; + } + } + } } });