cobolx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui.rs ADDED
@@ -0,0 +1,2 @@
1
+ pub mod draw;
2
+ pub mod tui;
@@ -0,0 +1,192 @@
1
+ use std::fs::{self, File};
2
+ use std::io::Write;
3
+ use tempfile::tempdir;
4
+
5
+ #[test]
6
+ fn init_indexer_persists_program_copybook_and_call_edges() {
7
+ let dir = tempdir().unwrap();
8
+ let copy_dir = dir.path().join("copy");
9
+ fs::create_dir_all(&copy_dir).unwrap();
10
+
11
+ File::create(dir.path().join("MAIN.cbl"))
12
+ .unwrap()
13
+ .write_all(
14
+ br#"
15
+ IDENTIFICATION DIVISION.
16
+ PROGRAM-ID. MAIN.
17
+ DATA DIVISION.
18
+ WORKING-STORAGE SECTION.
19
+ 01 WS-NEXT-PGM PIC X(8).
20
+ COPY CUSTOMER.
21
+ PROCEDURE DIVISION.
22
+ CALL "SUB001"
23
+ USING WS-NEXT-PGM.
24
+ CALL WS-NEXT-PGM.
25
+ STOP RUN.
26
+ "#,
27
+ )
28
+ .unwrap();
29
+
30
+ File::create(dir.path().join("SUB001.cbl"))
31
+ .unwrap()
32
+ .write_all(
33
+ br#"
34
+ IDENTIFICATION DIVISION.
35
+ PROGRAM-ID. SUB001.
36
+ PROCEDURE DIVISION.
37
+ EXIT PROGRAM.
38
+ "#,
39
+ )
40
+ .unwrap();
41
+
42
+ File::create(dir.path().join("CUSTOMER.cpy"))
43
+ .unwrap()
44
+ .write_all(
45
+ br#"
46
+ 01 CUSTOMER-REC.
47
+ 05 CUST-ID PIC X(10).
48
+ 05 CUST-BALANCE PIC S9(7)V99
49
+ COMP-3.
50
+ 05 CUST-ALIAS REDEFINES CUST-ID PIC X(10).
51
+ 05 CUST-ADDR OCCURS
52
+ 3 TIMES
53
+ PIC X(20).
54
+ "#,
55
+ )
56
+ .unwrap();
57
+
58
+ let mut store = rdo::memory::MemoryStore::open_or_create(dir.path()).unwrap();
59
+ let report = rdo::cobol::indexer::index_sandbox(dir.path(), &mut store).unwrap();
60
+
61
+ assert_eq!(report.source_count, 2);
62
+ assert_eq!(report.copybook_count, 1);
63
+ assert_eq!(report.programs.len(), 2);
64
+ assert_eq!(report.copybook_uses, 1);
65
+ assert_eq!(report.resolved_copybooks, 1);
66
+ assert_eq!(report.static_calls, 1);
67
+ assert_eq!(report.dynamic_calls, 1);
68
+ assert_eq!(report.data_items, 6);
69
+
70
+ let conn = store.connection();
71
+ let programs: i64 = conn
72
+ .query_row("SELECT COUNT(*) FROM programs", [], |row| row.get(0))
73
+ .unwrap();
74
+ let copies: i64 = conn
75
+ .query_row(
76
+ "SELECT COUNT(*) FROM copybook_uses WHERE resolve_status = 'resolved'",
77
+ [],
78
+ |row| row.get(0),
79
+ )
80
+ .unwrap();
81
+ let static_calls: i64 = conn
82
+ .query_row(
83
+ "SELECT COUNT(*) FROM call_edges WHERE kind = 'static'",
84
+ [],
85
+ |row| row.get(0),
86
+ )
87
+ .unwrap();
88
+ let dynamic_calls: i64 = conn
89
+ .query_row(
90
+ "SELECT COUNT(*) FROM call_edges WHERE kind = 'dynamic'",
91
+ [],
92
+ |row| row.get(0),
93
+ )
94
+ .unwrap();
95
+ let data_items: i64 = conn
96
+ .query_row("SELECT COUNT(*) FROM data_items", [], |row| row.get(0))
97
+ .unwrap();
98
+ let cust_id_pic: String = conn
99
+ .query_row(
100
+ "SELECT pic FROM data_items WHERE name = 'CUST-ID'",
101
+ [],
102
+ |row| row.get(0),
103
+ )
104
+ .unwrap();
105
+ let comp3_usage: String = conn
106
+ .query_row(
107
+ "SELECT usage_clause FROM data_items WHERE name = 'CUST-BALANCE'",
108
+ [],
109
+ |row| row.get(0),
110
+ )
111
+ .unwrap();
112
+ let comp3_pic: String = conn
113
+ .query_row(
114
+ "SELECT pic FROM data_items WHERE name = 'CUST-BALANCE'",
115
+ [],
116
+ |row| row.get(0),
117
+ )
118
+ .unwrap();
119
+ let redefines: String = conn
120
+ .query_row(
121
+ "SELECT redefines FROM data_items WHERE name = 'CUST-ALIAS'",
122
+ [],
123
+ |row| row.get(0),
124
+ )
125
+ .unwrap();
126
+ let occurs: i64 = conn
127
+ .query_row(
128
+ "SELECT occurs FROM data_items WHERE name = 'CUST-ADDR'",
129
+ [],
130
+ |row| row.get(0),
131
+ )
132
+ .unwrap();
133
+ let ws_layout: (i64, i64, String) = conn
134
+ .query_row(
135
+ "SELECT byte_offset, byte_size, storage_kind FROM data_items WHERE name = 'WS-NEXT-PGM'",
136
+ [],
137
+ |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
138
+ )
139
+ .unwrap();
140
+ let customer_group: (i64, i64, String) = conn
141
+ .query_row(
142
+ "SELECT byte_offset, byte_size, storage_kind FROM data_items WHERE name = 'CUSTOMER-REC'",
143
+ [],
144
+ |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
145
+ )
146
+ .unwrap();
147
+ let cust_id_layout: (i64, i64) = conn
148
+ .query_row(
149
+ "SELECT byte_offset, byte_size FROM data_items WHERE name = 'CUST-ID'",
150
+ [],
151
+ |row| Ok((row.get(0)?, row.get(1)?)),
152
+ )
153
+ .unwrap();
154
+ let comp3_layout: (i64, i64, String) = conn
155
+ .query_row(
156
+ "SELECT byte_offset, byte_size, storage_kind FROM data_items WHERE name = 'CUST-BALANCE'",
157
+ [],
158
+ |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
159
+ )
160
+ .unwrap();
161
+ let alias_layout: (i64, i64) = conn
162
+ .query_row(
163
+ "SELECT byte_offset, byte_size FROM data_items WHERE name = 'CUST-ALIAS'",
164
+ [],
165
+ |row| Ok((row.get(0)?, row.get(1)?)),
166
+ )
167
+ .unwrap();
168
+ let occurs_layout: (i64, i64) = conn
169
+ .query_row(
170
+ "SELECT byte_offset, byte_size FROM data_items WHERE name = 'CUST-ADDR'",
171
+ [],
172
+ |row| Ok((row.get(0)?, row.get(1)?)),
173
+ )
174
+ .unwrap();
175
+
176
+ assert_eq!(programs, 2);
177
+ assert_eq!(copies, 1);
178
+ assert_eq!(static_calls, 1);
179
+ assert_eq!(dynamic_calls, 1);
180
+ assert_eq!(data_items, 6);
181
+ assert_eq!(cust_id_pic, "X(10)");
182
+ assert_eq!(comp3_pic, "S9(7)V99");
183
+ assert_eq!(comp3_usage, "COMP-3");
184
+ assert_eq!(redefines, "CUST-ID");
185
+ assert_eq!(occurs, 3);
186
+ assert_eq!(ws_layout, (0, 8, "display".to_string()));
187
+ assert_eq!(customer_group, (8, 75, "group".to_string()));
188
+ assert_eq!(cust_id_layout, (8, 10));
189
+ assert_eq!(comp3_layout, (18, 5, "packed-decimal".to_string()));
190
+ assert_eq!(alias_layout, (8, 10));
191
+ assert_eq!(occurs_layout, (23, 60));
192
+ }
@@ -0,0 +1,21 @@
1
+ use tempfile::tempdir;
2
+
3
+ #[test]
4
+ fn first_open_creates_memory_database_and_schema() {
5
+ let dir = tempdir().unwrap();
6
+
7
+ let store = rdo::memory::MemoryStore::open_or_create(dir.path()).unwrap();
8
+
9
+ assert!(store.db_path().exists());
10
+
11
+ let count: i64 = store
12
+ .connection()
13
+ .query_row(
14
+ "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN ('files', 'programs', 'runs', 'skills')",
15
+ [],
16
+ |row| row.get(0),
17
+ )
18
+ .unwrap();
19
+
20
+ assert_eq!(count, 4);
21
+ }
@@ -0,0 +1,72 @@
1
+ use rdo::memory::MemoryStore;
2
+ use tempfile::tempdir;
3
+
4
+ #[test]
5
+ fn markdown_files_are_created_appended_and_read_under_docs_dir() {
6
+ let dir = tempdir().unwrap();
7
+ let store = MemoryStore::open_or_create(dir.path()).unwrap();
8
+
9
+ let path = store
10
+ .write_markdown("analysis/init.md", "# Init\n")
11
+ .unwrap();
12
+ store
13
+ .append_markdown("analysis/init.md", "\nCOBOL inventory ready.\n")
14
+ .unwrap();
15
+
16
+ assert!(path.starts_with(store.docs_dir()));
17
+ assert!(path.starts_with(dir.path().join("docs")));
18
+ assert_eq!(
19
+ store.read_markdown("analysis/init.md").unwrap(),
20
+ "# Init\n\nCOBOL inventory ready.\n"
21
+ );
22
+ }
23
+
24
+ #[test]
25
+ fn skill_files_are_created_and_read_under_skills_dir() {
26
+ let dir = tempdir().unwrap();
27
+ let store = MemoryStore::open_or_create(dir.path()).unwrap();
28
+
29
+ let path = store
30
+ .write_skill_file("cobol-migration/SKILL.md", "# COBOL Migration\n")
31
+ .unwrap();
32
+
33
+ assert!(path.starts_with(store.skills_dir()));
34
+ assert_eq!(
35
+ store.read_skill_file("cobol-migration/SKILL.md").unwrap(),
36
+ "# COBOL Migration\n"
37
+ );
38
+ }
39
+
40
+ #[test]
41
+ fn windows_style_relative_paths_are_normalized() {
42
+ let dir = tempdir().unwrap();
43
+ let store = MemoryStore::open_or_create(dir.path()).unwrap();
44
+
45
+ let path = store
46
+ .write_markdown("analysis\\windows.md", "# Windows\n")
47
+ .unwrap();
48
+
49
+ assert!(path.starts_with(dir.path().join("docs")));
50
+ assert_eq!(
51
+ store.read_markdown("analysis/windows.md").unwrap(),
52
+ "# Windows\n"
53
+ );
54
+ }
55
+
56
+ #[test]
57
+ fn project_file_writer_rejects_unsafe_or_wrong_paths() {
58
+ let dir = tempdir().unwrap();
59
+ let store = MemoryStore::open_or_create(dir.path()).unwrap();
60
+
61
+ assert!(store.write_markdown("../escape.md", "bad").is_err());
62
+ assert!(store.write_markdown("..\\escape.md", "bad").is_err());
63
+ assert!(store.write_markdown("C:\\tmp\\escape.md", "bad").is_err());
64
+ assert!(store.write_markdown("\\\\srv\\share\\x.md", "bad").is_err());
65
+ assert!(store.write_markdown("CON.md", "bad").is_err());
66
+ assert!(
67
+ store
68
+ .write_markdown("notes/not-markdown.txt", "bad")
69
+ .is_err()
70
+ );
71
+ assert!(store.write_skill_file("../SKILL.md", "bad").is_err());
72
+ }
@@ -0,0 +1,178 @@
1
+ use std::fs::{self, File};
2
+ use std::io::Write;
3
+ use tempfile::tempdir;
4
+
5
+ #[test]
6
+ fn test_scan_finds_cobol_files_flat() {
7
+ let dir = tempdir().unwrap();
8
+
9
+ // Create mock COBOL files with various supported extensions
10
+ File::create(dir.path().join("main.cbl"))
11
+ .unwrap()
12
+ .write_all(b"IDENTIFICATION DIVISION.")
13
+ .unwrap();
14
+ File::create(dir.path().join("utility.cpy"))
15
+ .unwrap()
16
+ .write_all(b"01 WS-VAR PIC X.")
17
+ .unwrap();
18
+ File::create(dir.path().join("test.cob"))
19
+ .unwrap()
20
+ .write_all(b"PROCEDURE DIVISION.")
21
+ .unwrap();
22
+ File::create(dir.path().join("other.coo"))
23
+ .unwrap()
24
+ .write_all(b"DATA DIVISION.")
25
+ .unwrap();
26
+
27
+ // Create non-COBOL files that should be ignored
28
+ File::create(dir.path().join("README.md")).unwrap();
29
+ File::create(dir.path().join("Cargo.toml")).unwrap();
30
+
31
+ let result = rdo::cobol::scanner::scan_sandbox(dir.path()).unwrap();
32
+
33
+ assert_eq!(result.len(), 4);
34
+
35
+ let sources: Vec<_> = result
36
+ .iter()
37
+ .filter(|f| f.file_type == rdo::cobol::scanner::CobolFileType::Source)
38
+ .collect();
39
+ let copybooks: Vec<_> = result
40
+ .iter()
41
+ .filter(|f| f.file_type == rdo::cobol::scanner::CobolFileType::Copybook)
42
+ .collect();
43
+
44
+ assert_eq!(
45
+ sources.len(),
46
+ 3,
47
+ "Should find 3 source files (.cbl, .cob, .coo)"
48
+ );
49
+ assert_eq!(copybooks.len(), 1, "Should find 1 copybook file (.cpy)");
50
+ }
51
+
52
+ #[test]
53
+ fn test_scan_recursive_finds_nested_files() {
54
+ let dir = tempdir().unwrap();
55
+
56
+ // Create nested directory structure
57
+ let sub1 = dir.path().join("module_a");
58
+ let sub2 = dir.path().join("module_b");
59
+ let sub2_nested = sub2.join("submodule");
60
+ fs::create_dir_all(&sub1).unwrap();
61
+ fs::create_dir_all(&sub2_nested).unwrap();
62
+
63
+ File::create(dir.path().join("main.cbl"))
64
+ .unwrap()
65
+ .write_all(b"ROOT")
66
+ .unwrap();
67
+ File::create(sub1.join("helper.cbl"))
68
+ .unwrap()
69
+ .write_all(b"SUB1")
70
+ .unwrap();
71
+ File::create(sub2.join("process.cob"))
72
+ .unwrap()
73
+ .write_all(b"SUB2")
74
+ .unwrap();
75
+ File::create(sub2_nested.join("deep.cpy"))
76
+ .unwrap()
77
+ .write_all(b"DEEP")
78
+ .unwrap();
79
+
80
+ let result = rdo::cobol::scanner::scan_sandbox(dir.path()).unwrap();
81
+
82
+ assert_eq!(
83
+ result.len(),
84
+ 4,
85
+ "Should find all 4 COBOL files across nested dirs"
86
+ );
87
+
88
+ // Verify paths are sorted
89
+ for i in 1..result.len() {
90
+ assert!(
91
+ result[i - 1].path <= result[i].path,
92
+ "Results should be sorted by path"
93
+ );
94
+ }
95
+ }
96
+
97
+ #[test]
98
+ fn test_scan_excludes_hidden_and_build_dirs() {
99
+ let dir = tempdir().unwrap();
100
+
101
+ // Create directories that should be excluded
102
+ let git_dir = dir.path().join(".git");
103
+ let target_dir = dir.path().join("target");
104
+ let node_modules = dir.path().join("node_modules");
105
+ let vendor_dir = dir.path().join("vendor");
106
+ let build_dir = dir.path().join("build");
107
+ let hidden_dir = dir.path().join(".hidden");
108
+ let valid_dir = dir.path().join("src");
109
+
110
+ for d in &[
111
+ &git_dir,
112
+ &target_dir,
113
+ &node_modules,
114
+ &vendor_dir,
115
+ &build_dir,
116
+ &hidden_dir,
117
+ &valid_dir,
118
+ ] {
119
+ fs::create_dir_all(d).unwrap();
120
+ }
121
+
122
+ // Put COBOL files in excluded dirs (should NOT be found)
123
+ File::create(git_dir.join("hooks.cbl")).unwrap();
124
+ File::create(target_dir.join("out.cbl")).unwrap();
125
+ File::create(node_modules.join("dep.cbl")).unwrap();
126
+ File::create(vendor_dir.join("lib.cbl")).unwrap();
127
+ File::create(build_dir.join("gen.cbl")).unwrap();
128
+ File::create(hidden_dir.join("secret.cbl")).unwrap();
129
+
130
+ // Put COBOL files in valid dirs (SHOULD be found)
131
+ File::create(dir.path().join("root.cbl"))
132
+ .unwrap()
133
+ .write_all(b"ROOT")
134
+ .unwrap();
135
+ File::create(valid_dir.join("app.cob"))
136
+ .unwrap()
137
+ .write_all(b"APP")
138
+ .unwrap();
139
+
140
+ let result = rdo::cobol::scanner::scan_sandbox(dir.path()).unwrap();
141
+
142
+ assert_eq!(
143
+ result.len(),
144
+ 2,
145
+ "Should only find files outside excluded directories"
146
+ );
147
+
148
+ let names: Vec<String> = result
149
+ .iter()
150
+ .map(|f| f.path.file_name().unwrap().to_string_lossy().into_owned())
151
+ .collect();
152
+ assert!(names.contains(&"root.cbl".to_string()));
153
+ assert!(names.contains(&"app.cob".to_string()));
154
+ }
155
+
156
+ #[test]
157
+ fn test_scan_empty_directory() {
158
+ let dir = tempdir().unwrap();
159
+
160
+ let result = rdo::cobol::scanner::scan_sandbox(dir.path()).unwrap();
161
+ assert!(result.is_empty(), "Empty dir should return no results");
162
+ }
163
+
164
+ #[test]
165
+ fn test_scan_tracks_file_size() {
166
+ let dir = tempdir().unwrap();
167
+
168
+ let content = b"IDENTIFICATION DIVISION.\nPROGRAM-ID. HELLO.\n";
169
+ File::create(dir.path().join("sized.cbl"))
170
+ .unwrap()
171
+ .write_all(content)
172
+ .unwrap();
173
+
174
+ let result = rdo::cobol::scanner::scan_sandbox(dir.path()).unwrap();
175
+
176
+ assert_eq!(result.len(), 1);
177
+ assert_eq!(result[0].size_bytes, content.len() as u64);
178
+ }