Feat/catalog number fallback - #45
Merged
Merged
Conversation
…e table The ACD binary stores hardware only as (vendor, product_type, product_code); the catalog number is not in the file. ModuleBuilder resolved it via CATALOG_NUMBERS.get(..., "") so any unlisted module emitted CatalogNumber="" and its hardware identity was lost from the L5X. Add catalog_number_for_identity() as the single resolution choke point: 1. caller/built-in table, 2. caller-supplied fallback, 3. structured CIP-<vendor>-<type>-<code> placeholder. (0,0,0) still returns "" so "no identity" stays distinguishable from "identity present but unknown". ModuleBuilder now calls the helper, so a better catalog or CIP-registry lookup is a one-line change. Adds test_catalog_numbers.py (pure-function, no ACD file / shared build/ dir, so it runs on Windows). Verified on CuteLogix.ACD: the CPU module (1/14/216) now exports CatalogNumber="CIP-1-14-216" instead of blank.
Two pre-existing, Windows-revealing bugs in the "dump Comps database to a
navigable folder tree" feature (DumpCompsRecordsToFile / DumpCompsRecords),
plus the shared build/ temp-dir collision. All three broke the test suite on
Windows and the README-documented flows:
1. DumpCompsRecords named one sub-dir + <name>.dat per Comp record after
comp_name. Logix comp_names can contain ":" (channel names like
"CHANNEL_DI_TIMESTAMP:O:0") and "/", "\\", which are illegal in Windows
file/directory names -> OSError [WinError 123] in os.makedirs/open.
Add _sanitize_path_component() (illegal chars -> "_", reserved device
names prefixed, trailing space/dot stripped) and use it for the on-disk
path; the original name is kept in output.log so nothing is lost.
2. DumpCompsRecordsToFile.extract opened output.log without creating
output_directory -> FileNotFoundError on the README's own
DumpCompsRecordsToFile("My.ACD", "output/"). Add os.makedirs(...).
Also the record tree defaulted to ./dump, separate from output_directory;
it now writes into output_directory so the tree and log land together.
3. ExportL5x._temp_dir defaulted to the shared relative dir "build", so
concurrent/parallel test runs collided on build/acd.db (Windows locks the
SQLite file while a connection is open, so a second run's os.remove in
__post_init__ failed). Default is now tempfile.mkdtemp() per instance
(the documented intent in the original comment); pass a specific dir to
restore the old behaviour for manual inspection.
Tests: test_dump_path_sanitize.py (pure-function coverage of the sanitizer)
and test_dump_fresh_dir.py (the full README flow on a fresh tmp dir: tree +
log land together, no path-illegal names). Full suite is now green on
Windows: 113 passed, 0 failed, 2 skipped (was 98 passed with failures).
Verified end-to-end on resources/CuteLogix.ACD: 7074-record tree + 294KB
output.log created together, zero filenames containing path-illegal chars.
… data
Adds an extension point so a richer/shared catalog (Rockwell's own data, or
the module templates a-colleagues-internal-tool extracts) can be added as DATA, not code.
This is the "give-back" seam between hutcheb/acd and a-colleagues-internal-tool: both can
share one CIP-identity -> catalog-number mapping without either owning it.
New in acd/l5x/catalog_numbers.py:
- load_external_catalog(path): load + validate a JSON catalog. Keys are
"vendor:product_type:product_code" (or a 3-array / object form); values
must be non-empty strings; every identity must be three non-negative
ints. "_"-prefixed keys are treated as metadata and skipped, so a file can
carry a human-readable note and still load. Malformed input raises
CatalogError (a data error the caller can fix) rather than silently
producing wrong catalog numbers.
- merge_catalog(base, overrides): base (default CATALOG_NUMBERS) with
overrides applied on top; neither input mutated. External entries win on
conflict -- the natural "extend/patch" semantics.
- catalog_number_for_identity() gains a documented example of loading an
external catalog and passing it as table=.
The resolution order in catalog_number_for_identity is unchanged: table ->
caller fallback -> CIP-<v>-<t>-<c> placeholder. A merged table simply becomes
"table", so nothing else in the conversion path changes.
Ships resources/external_catalog.example.json with the two entries that are
verifiable from the bundled L5X samples (1:12:166 -> 1756-EN2T,
1:14:168 -> 1756-L85E) as a working template to extend.
Tests: test_catalog_external.py (10 cases: colon/array/entries-wrappers,
metadata skip, bad value/key rejection, merge, external-wins resolution, the
realistic load->merge->resolve flow, and that the shipped example file itself
loads). Full suite green on Windows: 123 passed, 0 failed, 2 skipped.
Adds a shape/invariant test on the built-in catalog table so a future edit
that accidentally drops, corrupts, or mis-types an entry is caught. It tests
what the table IS (honest, not a guess): every key is a 3-tuple of
non-negative ints, every value is a non-empty string, and the table is the
documented size (39).
test/test_catalog_numbers.py:
- test_builtin_table_invariants: locks the table's shape (39 entries, key
shape, value type). Deliberately does NOT flag two distinct triples
sharing a catalog value (e.g. firmware variants of the same device), which
is legitimate and present in the current table.
Suite: 124 passed, 2 skipped (was 123 passed, 2 skipped). Pure-function test,
no ACD file, no temp dir, runs reliably on Windows.
The `python -m acd.l5x.export_l5x <in.ACD> <out.L5X>` entry point built the
SQLite database (and logged the full extraction) but never wrote the .L5X.
Root cause: the __main__ block passed the output path as ExportL5x's SECOND
POSITIONAL arg, which landed in the `_temp_dir` dataclass field (a directory,
not a file) -- so the output path was silently discarded and nothing was
emitted. Running the command left the output dir empty.
acd/l5x/export_l5x.py (__main__):
- Build ExportL5x(input) (one positional arg, as the dataclass defines),
fetch `export.project`, serialise via project.to_xml(), and write the file
(creating the output dir if needed). This mirrors ConvertAcdToL5x.extract()
in acd.api, the path the workbench already uses successfully.
Verified:
- `python -m acd.l5x.export_l5x resources\ACDTestsWithAOI.ACD
out\ACDTestsWithAOI.L5X` now writes out\ACDTestsWithAOI.L5X (16,801 bytes),
log line "Wrote L5X: out\ACDTestsWithAOI.L5X".
- Modules section matches the bundled reference exactly:
Local = CatalogNumber 1756-L85E (1/14/168), ETH_MODULE = 1756-EN2T
(1/12/166) -- both in-table, unchanged by the catalog-fallback commit.
- CuteLogix: `resources\CuteLogix.ACD` -> Local module now reads
CatalogNumber="CIP-1-14-216" Vendor=1 ProductType=14 ProductCode=216
(the fallback, since 1/14/216 is not in the table) -- the A3 test piece.
- acd catalog tests still green (test_catalog_numbers + test_catalog_external:
17 passed).
This is what the acd-branch checklist A1/A2 step depended on; the command now
behaves as documented.
A3 finding (Studio 5000 v35 import, verified 2026-09-02): importing a
CuteLogix-converted L5X (CPU = 1756-L82/5580, CIP 1/14/216, NOT in the built-in
catalog) made Studio read the controller as 1756-L1 / ControlLogix 5550 and
raise "The controller configured for this project is not supported in this
revision of software" -> "Change Controller Type".
Root cause: ControllerBuilder set the controller ProcessorType to the root
module's catalog_number. For an in-table CPU that's a real part number
(1756-L85E) -- fine. For an out-of-table CPU it was our CIP-triple placeholder
(CIP-1-14-216), which Studio CANNOT use as a ProcessorType; it then mis-guesses
the CPU type. The distinction the fallback missed:
- a module's CatalogNumber may carry a CIP-... placeholder (harmless -- Studio
retains it as a custom catalog number, import succeeds);
- the controller ProcessorType must be a real Rockwell catalog number, or
absent -- Studio prompts for the controller type rather than silently
picking a wrong one.
acd/l5x/elements.py:
- _is_cip_placeholder(catalog) -- recognises the "CIP-<v>-<t>-<c>" fallback
string (the form catalog_number_for_identity produces) and returns False for
real catalog numbers.
- ControllerBuilder: when the CPU's catalog_number is a CIP placeholder (or
empty), emit processor_type=None (omitted from XML) instead of the CIP
string. In-table CPUs are unchanged (still emit the real ProcessorType).
The module-level CatalogNumber keeps the CIP placeholder in both cases.
Verified:
- CuteLogix (out-of-table CPU 1/14/216): ProcessorType now ABSENT; module
CatalogNumber still CIP-1-14-216 with Vendor/ProductType/ProductCode.
- ACDTestsWithAOI (in-table CPU 1756-L85E): ProcessorType still 1756-L85E
(unchanged -- no regression for in-table CPUs).
- New test test_is_cip_placeholder_recognises_the_fallback_not_real_catalogs
locks both sides of the predicate.
- Full suite: 125 passed, 2 skipped (was 124) -- no golden-output regressions
(none of the in-table-CPU fixtures lost their ProcessorType).
This makes an out-of-table CPU degrade to "Studio asks you for the controller
type" instead of "Studio silently picks 1756-L1". The module-level CIP fallback
(the original intent of this branch) is preserved. See docs/acd-branch-pr-draft.md
section 1a (A3 FINDING) for the full context and the follow-up options (cover the
CPU via the external catalog; ship a Rockwell CIP-identity list for the 1756-Lx
family).
… the import)
Reverts the "omit ProcessorType for an out-of-table CPU" change from 662561b.
A second Studio 5000 v35 import test showed that omitting ProcessorType makes
Studio unable to CREATE the project file at all:
"Failed to create project file '...\CuteLogic.ACD'. Couldn't be found."
(Error 716-80042001), with the import line reporting "Complete - 1 error(s)".
That is a WORSE outcome than keeping the placeholder: with the CIP-... string as
ProcessorType, the import completes, Studio shows "Change Controller Type"
(observed: it defaulted to 1756-L1/5550), the user picks the real CPU (e.g.
1756-L82E), and the project saves. So the placeholder degrades to a manual
"Change Controller Type" step, which WORKS.
acd/l5x/elements.py:
- ControllerBuilder: restore emitting the root module's catalog_number as
ProcessorType (placeholder included) -- do NOT set it to None for an
out-of-table CPU.
- _is_cip_placeholder + _CIP_PLACEHOLDER_RE are RETAINED (tested) as the
predicate for the real fix path: cover the CPU in the external catalog so
catalog_number_for_identity resolves to a real number.
Verified:
- CuteLogix (out-of-table CPU 1/14/216): ProcessorType = CIP-1-14-216
(the working, manually-correctable behavior).
- ACDTestsWithAOI (in-table CPU 1756-L85E): ProcessorType = 1756-L85E
(unchanged).
- Full suite: 125 passed, 2 skipped (no regressions).
Note: the "Failed to create project file ... Couldn't be found" (Error
716-80042001) at Save As that first appeared alongside this is a SEPARATE
environment issue -- Studio's default save path points at C:\Users\admin\...
(a profile that doesn't exist on this machine; Studio is being migrated from a
VM). Override the Save-As location to a real folder. It is NOT caused by the
L5X content. See docs/acd-branch-pr-draft.md section 1a (A3 FINDING + follow-up)
and docs/acd-branch-validation.md for the corrected A3 conclusion and the
corrected recommendation (option 1 + 3, NOT option 2).
…ries)
Adds resources/external_catalog.enginerom.json -- a REAL external catalog
generated from a genuine Studio 5000 backup (Engine_Room.L5X, a ControlLogix
1756-L74 project) via the Workbench L5X->CIP-triple extractor
(tools/l5x-catalog.js --acd). Every entry is taken verbatim from the real L5X
(not fabricated):
1:14:95 -> 1756-L74 (CPU, ControlLogix 5570)
1:12:166 -> 1756-EN2T
1:12:18 -> 1756-DHRIO/B
1:12:51 -> RIO-ADAPTER
1:103:1 -> RIO-MODULE
1:7:30 -> 1756-OW16I
1:10:6 -> 1756-IF16/A
1:12:14 -> 1756-DNB
1:12:7 -> 1756-CNB/E
1:12:70 -> 1788-CN2DN
1:12:22 -> 1756-CN2R/B
This is the C4 catalog give-back demonstrated on real data: load it through
load_external_catalog() and merge over CATALOG_NUMBERS, and an ACD whose CPU is
a 1756-L74 (CIP 1/14/95) resolves ProcessorType to the real "1756-L74" instead
of the CIP-... placeholder -- closing the A3 "Change Controller Type" step for
that CPU family. Identities not in this file (e.g. CuteLogix's 1/14/216) still
fall back to CIP-... (the manual-step case).
Verified:
- Loads through the real Python load_external_catalog (11 entries); merge over
the built-in table resolves 1:14:95 -> 1756-L74 (and leaves in-table entries
like 1:12:166 -> 1756-EN2T unchanged).
- The Workbench test-real-backup.js opens the source L5X (420 tags, 19 modules,
23 programs, 2104 rungs; CPU 1756-L74 CIP 1:14:95) -- REAL-BACKUP PASS.
Usage: pass this file as the external catalog to the ACD conversion (merge over
the built-in table), or merge it into a shared catalog with other real backups.
…fix)
Wires the external-catalog seam (load_external_catalog / merge_catalog) into
the actual conversion, so an out-of-table CPU/module resolves to a REAL part
number instead of the CIP-... placeholder. This is the A3 fix made
data-driven: the A3 finding was that an out-of-table CPU exported
ProcessorType="CIP-..." which Studio can't recognise (it shows "Change
Controller Type"); covering the CPU in an external catalog makes the
conversion emit a real ProcessorType and the L5X imports cleanly.
acd/api.py:
- _resolve_catalog_table(catalog_file): None -> None (built-in only,
unchanged); dict -> merged over built-in; path -> load_external_catalog +
merge. Returns the merged table the builders use.
- ImportProjectFromFile.catalog_file, ConvertAcdToL5x.catalog_file, and
load_acd(catalog_file=...) all accept the optional catalog and pass it
through to ExportL5x.
acd/l5x/export_l5x.py:
- ExportL5x._catalog_table (None default). The controller property passes it
to ControllerBuilder by KEYWORD (the field is 3rd after _cur/_object_id,
so a positional arg would have landed in _object_id -- that bug was caught
and fixed).
acd/l5x/elements.py:
- ControllerBuilder._catalog_table + ModuleBuilder._catalog_table (None
default). ModuleBuilder.build passes table=self._catalog_table to
catalog_number_for_identity; ControllerBuilder passes it to each
ModuleBuilder, so the CPU module's catalog_number (and thus the controller
ProcessorType, derived from it) uses the merged table.
Verified (in-repo, no Studio):
- END-TO-END: ConvertAcdToL5x(CuteLogix.ACD, catalog_file={1:14:216 ->
1756-L82}) emits Controller ProcessorType="1756-L82" and module
CatalogNumber="1756-L82" (real) instead of CIP-1-14-216. Control: without
the catalog the same ACD emits ProcessorType="CIP-1-14-216" (the manual
step). So a CPU covered by the external catalog imports cleanly in Studio.
- New tests: test_resolve_catalog_table_helper (None/dict/path) and
test_builder_threads_catalog_table (builders carry + use the table).
- Backward compatible: no catalog arg -> built-in table only, all prior
behaviour unchanged (full suite green).
This makes the C4 catalog give-back (real L5X -> external catalog, e.g.
external_catalog.enginerom.json from Engine_Room.L5X) usable at conversion time:
convert an ACD with its real catalog loaded, and covered CPUs/modules resolve to
real part numbers.
Two real Studio 5000 import bugs, found by importing acd-generated L5X in Studio
v35 and comparing against the known-good reference files in resources/ (which
import cleanly). Both were in the writer; the reference files already had the
correct structure, so the writer did not match its own golden files.
Bug 1 -- wrong <Controller> child element order
Studio: "Element <AddOnInstructionDefinitions> is in the wrong order."
The base L5xElement.to_xml() emits list sections in dataclass field order
(DataTypes, Modules, Tags, Programs, Tasks, AddOnInstructionDefinitions), and
Controller.to_xml() then APPENDED RedundancyInfo/Security/SafetyInfo after the
data sections. The Studio schema requires:
Description, RedundancyInfo, Security, SafetyInfo, DataTypes, Modules,
AddOnInstructionDefinitions, Tags, Programs, Tasks, CST, WallClockTime,
Trends, DataLogs, TimeSynchronize, EthernetPorts
i.e. the structural stubs come BEFORE the data sections, and
AddOnInstructionDefinitions comes right AFTER Modules (not after Tasks).
Fix: Controller.to_xml() now builds the inner content explicitly in the schema
order (via a _section_xml helper), instead of relying on field order + append.
Bug 2 -- empty <Ports/> for CPUs missing from PORT_STRUCTURES
Studio: "Required property 'Port' was missing ... Module[@name='Local']/Ports"
Module._build_ports_xml() falls back to <Ports/> when the CIP triple is not in
PORT_STRUCTURES. The 1756-L82E (CIP 1/14/92) was missing, so an L82E CPU
exported an empty <Ports/>.
Fix: added (1,14,92) to PORT_STRUCTURES (ICP + Ethernet, same structure as the
L85E) so an L82E CPU gets a proper <Ports> section.
Verified (in-repo, validated against the reference files):
- test_l5x_controller_schema.py (new): asserts the generated <Controller> child
order == the schema order (matches ACDTestsWithAOI.L5X / Engine_Room.L5X),
that AddOnInstructionDefinitions follows Modules, that the structural stubs
precede DataTypes, that the root CPU has a non-empty <Ports> with an ICP port,
that the L82E is in PORT_STRUCTURES, and that the output is well-formed XML.
- ACDTestsWithAOI converted output now matches the reference's element order and
CPU ports exactly (order MATCH, CPU PORTS MATCH).
- Full acd pytest suite green.
Studio re-test (user): re-import T2_L82E_Catalog_Fixed.L5X (now with the correct
element order + L82E <Ports>) -- should import with NO "wrong order" and NO
"Port missing" errors, and resolve the controller to 1756-L82E.
…ernet
Third Studio 5000 import gap, found by importing the L82E test file in Studio v35:
even with the correct <Controller> element order and a populated <Ports> for the
CPU, Studio renamed+deleted the CPU module ("Collision ... renamed to Local1 /
Deleting module"). The known-good reference files (e.g. ACDTestsWithAOI.L5X) carry
an <EthernetPorts> descriptor for the CPU's integrated Ethernet port:
<EthernetPorts><EthernetPort Port="1" Label="1" PortEnabled="true"/></EthernetPorts>
which acd's writer never emitted (it only produced an empty <EthernetPorts/>).
Controller._ethernet_ports_xml() now emits that descriptor when the root CPU has an
integrated Ethernet port (L83E/L84E/L85E/L82E/L86E etc. -- detected via
PORT_STRUCTURES), and an empty <EthernetPorts/> otherwise (e.g. L74, no integrated
Ethernet). One <EthernetPort> per integrated Ethernet port.
Verified: ProjectTemplate (L82E/L85E-equiv) converted output now carries
<EthernetPort Port="1" PortEnabled="true"/> matching the reference; full acd pytest
suite green.
Studio re-test: re-import T2_L82E_Catalog_Fixed.L5X (now 2210 bytes, with the
EthernetPort descriptor) -- the "Deleting module" collision should be gone and the
CPU should survive import.
…tible)
THE root cause of the persistent "Required property 'Port' was missing" Studio
import error on the L82E test file (and any ACD converted via ConvertAcdToL5x).
ConvertAcdToL5x.extract() defaulted to pretty_print=True, which runs
minidom.toprettyxml(indent=" "). That inserts blank/whitespace-only lines BETWEEN
the <Port> elements inside <Ports>. Studio 5000's strict L5X parser then reports
"Required property 'Port' was missing ... Module/Ports" at the </Ports> line --
even though the <Port> is present. The blank lines are the culprit; the reference
files in resources/ (which import cleanly) are NOT pretty-printed.
Fix: pretty_print now defaults to False, so the default ConvertAcdToL5x output is
the raw to_xml() format (no blank lines), matching the Studio-acceptable reference
files. pretty_print=True remains available (opt-in) for human reading.
Verified:
- ACDTestsWithAOI converted with the NEW default produces a <Ports> block with NO
blank/indent lines (byte-identical in structure to the reference).
- New tests: test_default_conversion_has_no_blank_lines_in_ports (the Studio-
compatibility guard) and test_pretty_print_is_an_opt_in.
- Full acd pytest suite green.
Studio re-test: import A3_L82E_NOPRETTY.L5X (1864 bytes, generated with
pretty_print=False) -- the "Port missing" error should be GONE and the L82E CPU
should survive import.
…14:92)
The earlier L82E port entry used CIP 1:14:92, which was a guess based on the
ProjectTemplate ACD's generic/template CPU -- NOT a real L82E. That identity
mismatch is why Studio rejected the L82E test module ("Port missing" -> delete):
Studio cross-checks ProductCode against its part database, and 92 is not an L82E.
The user supplied GENUINE L82E/L81E/L72 ACDs (samples/C1756L82E.ACD etc.). Their
QuickInfo.DeviceIdentity gives the REAL CIP identities:
C1756L82E.ACD -> VendorID=1 ProductType=14 ProductCode=165 (firmware 35.11/v35)
C1756L81E.ACD -> VendorID=1 ProductType=14 ProductCode=164
C1756L72.ACD -> VendorID=1 ProductType=14 ProductCode=93 (in built-in table)
So the real L82E is 1:14:165. This commit:
- removes the bogus (1,14,92) L82E port entry,
- adds the REAL (1,14,165) 1756-L82E and (1,14,164) 1756-L81E port structures
(ICP + integrated Ethernet, root CPU, same shape as the L85E 1:14:168).
The genuine L82E (1:14:165) is out of the built-in catalog AND the port table, so it
is now the definitive A3 test case:
- L82E_CIP.L5X (no catalog) -> ProcessorType=CIP-1-14-165 (manual step)
- L82E_FIXED.L5X (catalog 1:14:165 -> 1756-L82E) -> ProcessorType=1756-L82E,
Bus Size=10 (read from the real ACD's _chassis_size, not hardcoded), ICP +
Ethernet ports, EthernetPort descriptor.
- L72_clean.L5X (control, real L72 in table) -> clean import.
Verified:
- test_l82e_cpu_has_ports now asserts (1,14,165) is in PORT_STRUCTURES with ICP +
Ethernet, and that the bogus (1,14,92) is NOT (the mislabel guard).
- Full acd pytest suite green (133 passed, 2 skipped).
Studio re-test: import L82E_FIXED.L5X (genuine L82E identity 1:14:165, real
Bus Size) -- the "Port missing" / "Deleting module" should be gone and it should
import cleanly as 1756-L82E. L72_clean.L5X is the known-clean control.
The user supplied genuine L82E/L81E/L72 ACDs (samples/C1756L82E.ACD etc.). Their QuickInfo.DeviceIdentity gives the REAL CIP identities: C1756L82E.ACD -> VendorID=1 ProductType=14 ProductCode=165 C1756L81E.ACD -> VendorID=1 ProductType=14 ProductCode=164 C1756L72.ACD -> VendorID=1 ProductType=14 ProductCode=93 (already in table) Adding the real L82E (1:14:165) and L81E (1:14:164) to the built-in CATALOG_NUMBERS means a genuine L82E/L81E now resolves to its real catalog number NATIVELY (no external catalog needed) -- so the ACD->L5X conversion emits ProcessorType= "1756-L82E"/"1756-L81E" directly. This is verifiable data (from real ACDs), not a guess, and matches the port-structure entries added in 12a36cf. Verified: - catalog_number_for_identity((1,14,165)) == "1756-L82E", ((1,14,164)) == "1756-L81E" (native resolution). - test_builtin_table_invariants updated to 41 entries (was 39) + asserts the two new real identities are present. - Full acd pytest suite green. Studio re-test: import L82E_native.L5X (Desktop\A3_REAL\) -- the real L82E now resolves natively to 1756-L82E (ProductCode 165, real Bus Size 10) with no external catalog. Should import cleanly. L72_clean.L5X is the control; CuteLogix_CIP_fallback.L5X shows the CIP-... fallback for a CPU still out of the table (1:14:216).
…no production caller)
…e dead fallback kwarg
nokkies
force-pushed
the
feat/catalog-number-fallback
branch
from
September 3, 2026 06:08
33405db to
e8477af
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.