Skip to content

Commit 05a2979

Browse files
committed
Add unit tests for AICC, SCORM, and BaseParser components
Introduce comprehensive unit tests for `AiccDetectorPlugin`, `BaseModuleMetadata`, and `BaseParser` to validate functionality, including edge cases and exception handling. Ensure high coverage for detection logic, metadata extraction, and parser operations.
1 parent 216bd23 commit 05a2979

24 files changed

Lines changed: 4888 additions & 314 deletions

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ This is a Java library for parsing and validating eLearning module manifests in
99
- AICC (INI-based format)
1010
- cmi5 (XML-based)
1111

12+
## Code Style Guidelines
13+
14+
- Use imports whenever possible, avoiding fully qualified class references
15+
1216
## Build Commands
1317

1418
```bash

pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<version.slf4j>2.0.17</version.slf4j>
7373
<version.testcontainers>1.21.0</version.testcontainers>
7474
<version.threeten>1.8.0</version.threeten>
75+
<version.mockito>5.17.0</version.mockito>
7576
</properties>
7677

7778
<dependencyManagement>
@@ -172,6 +173,20 @@
172173
<scope>test</scope>
173174
</dependency>
174175

176+
<dependency>
177+
<groupId>org.mockito</groupId>
178+
<artifactId>mockito-core</artifactId>
179+
<version>${version.mockito}</version>
180+
<scope>test</scope>
181+
</dependency>
182+
183+
<dependency>
184+
<groupId>org.mockito</groupId>
185+
<artifactId>mockito-junit-jupiter</artifactId>
186+
<version>${version.mockito}</version>
187+
<scope>test</scope>
188+
</dependency>
189+
175190
<!-- Testcontainers Dependencies -->
176191
<dependency>
177192
<groupId>org.testcontainers</groupId>

src/main/java/dev/jcputney/elearning/parser/impl/DefaultModuleFileProvider.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,21 @@ public String getRootPath() {
110110
@Override
111111
public boolean hasXapiSupport() {
112112
log.debug("Checking for xAPI support");
113-
boolean xapiJsExists = fileAccess.fileExists(XAPI_JS_FILE);
114-
boolean sendStatementExists = fileAccess.fileExists(XAPI_SEND_STATEMENT_FILE);
113+
boolean xapiJsExists = false;
114+
boolean sendStatementExists = false;
115+
116+
try {
117+
xapiJsExists = fileAccess.fileExists(XAPI_JS_FILE);
118+
} catch (Exception e) {
119+
log.debug("Error checking for xAPI JS file: {}", e.getMessage());
120+
}
121+
122+
try {
123+
sendStatementExists = fileAccess.fileExists(XAPI_SEND_STATEMENT_FILE);
124+
} catch (Exception e) {
125+
log.debug("Error checking for xAPI Statement file: {}", e.getMessage());
126+
}
127+
115128
boolean hasXapi = xapiJsExists || sendStatementExists;
116129

117130
if (hasXapi) {

src/main/java/dev/jcputney/elearning/parser/impl/S3FileAccessV1.java

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.amazonaws.services.s3.model.S3Object;
2626
import com.amazonaws.services.s3.model.S3ObjectSummary;
2727
import dev.jcputney.elearning.parser.api.FileAccess;
28+
import dev.jcputney.elearning.parser.util.LogMarkers;
2829
import java.io.ByteArrayInputStream;
2930
import java.io.IOException;
3031
import java.io.InputStream;
@@ -39,11 +40,10 @@
3940
import java.util.stream.Collectors;
4041
import lombok.Getter;
4142
import lombok.extern.slf4j.Slf4j;
42-
import dev.jcputney.elearning.parser.util.LogMarkers;
4343

4444
/**
45-
* Optimized implementation of FileAccess using AWS S3 SDK v1 with batch operations,
46-
* streaming support, and intelligent caching.
45+
* Optimized implementation of FileAccess using AWS S3 SDK v1 with batch operations, streaming
46+
* support, and intelligent caching.
4747
*/
4848
@Slf4j
4949
@SuppressWarnings("unused")
@@ -59,7 +59,7 @@ public class S3FileAccessV1 implements FileAccess {
5959
private final AmazonS3 s3Client;
6060
private final String bucketName;
6161
private final ExecutorService executorService;
62-
62+
6363
// Caches for performance
6464
private final Map<String, Boolean> fileExistsCache = new ConcurrentHashMap<>();
6565
private final Map<String, List<String>> directoryListCache = new ConcurrentHashMap<>();
@@ -88,7 +88,7 @@ public S3FileAccessV1(AmazonS3 s3Client, String bucketName, String rootPath) {
8888

8989
// Lazy initialization of root path detection
9090
this.rootPath = processedPath;
91-
91+
9292
if (this.rootPath.endsWith("/")) {
9393
this.rootPath = this.rootPath.substring(0, this.rootPath.length() - 1);
9494
}
@@ -115,7 +115,7 @@ public Map<String, Boolean> fileExistsBatch(List<String> paths) {
115115
// Get cached results first
116116
Map<String, Boolean> results = new ConcurrentHashMap<>();
117117
List<String> uncachedPaths = new ArrayList<>();
118-
118+
119119
for (String path : paths) {
120120
Boolean cached = fileExistsCache.get(path);
121121
if (cached != null) {
@@ -124,17 +124,18 @@ public Map<String, Boolean> fileExistsBatch(List<String> paths) {
124124
uncachedPaths.add(path);
125125
}
126126
}
127-
127+
128128
// Check uncached paths in parallel
129129
if (!uncachedPaths.isEmpty()) {
130-
List<CompletableFuture<Map.Entry<String, Boolean>>> futures = uncachedPaths.stream()
130+
List<CompletableFuture<Map.Entry<String, Boolean>>> futures = uncachedPaths
131+
.stream()
131132
.map(path -> CompletableFuture.supplyAsync(() -> {
132133
boolean exists = checkFileExistsOnS3(path);
133134
fileExistsCache.put(path, exists);
134135
return Map.entry(path, exists);
135136
}, executorService))
136137
.toList();
137-
138+
138139
futures.forEach(future -> {
139140
try {
140141
Map.Entry<String, Boolean> result = future.join();
@@ -144,47 +145,54 @@ public Map<String, Boolean> fileExistsBatch(List<String> paths) {
144145
}
145146
});
146147
}
147-
148+
148149
return results;
149150
}
150151

151152
/**
152153
* Prefetch common module files in parallel for faster subsequent access.
153154
*/
154155
public void prefetchCommonFiles() {
155-
List<String> commonFiles = COMMON_MODULE_FILES.stream()
156+
List<String> commonFiles = COMMON_MODULE_FILES
157+
.stream()
156158
.filter(file -> !smallFileCache.containsKey(file))
157159
.toList();
158-
160+
159161
if (commonFiles.isEmpty()) {
160162
return;
161163
}
162-
164+
163165
log.debug(LogMarkers.S3_VERBOSE, "Prefetching {} common module files", commonFiles.size());
164-
165-
List<CompletableFuture<Void>> futures = commonFiles.stream()
166+
167+
List<CompletableFuture<Void>> futures = commonFiles
168+
.stream()
166169
.map(file -> CompletableFuture.runAsync(() -> {
167170
try {
168171
String fullFilePath = fullPath(file);
169172
if (checkFileExistsOnS3(file)) {
170173
// Get file size first
171174
long size = getFileSizeOnS3(file);
172175
fileSizeCache.put(file, size);
173-
176+
174177
// Only cache small files
175178
if (size <= STREAMING_THRESHOLD) {
176-
byte[] content = s3Client.getObjectAsString(bucketName, fullFilePath).getBytes();
179+
byte[] content = s3Client
180+
.getObjectAsString(bucketName, fullFilePath)
181+
.getBytes();
177182
smallFileCache.put(file, content);
178-
log.debug(LogMarkers.S3_VERBOSE, "Prefetched file: {} ({} bytes)", file, content.length);
183+
log.debug(LogMarkers.S3_VERBOSE, "Prefetched file: {} ({} bytes)", file,
184+
content.length);
179185
}
180186
}
181187
} catch (Exception e) {
182188
// Failed to prefetch file - this is expected for missing files
183189
}
184190
}, executorService))
185191
.toList();
186-
187-
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
192+
193+
CompletableFuture
194+
.allOf(futures.toArray(new CompletableFuture[0]))
195+
.join();
188196
}
189197

190198
/**
@@ -212,30 +220,33 @@ public InputStream getFileContentsInternal(String path) throws IOException {
212220
log.debug(LogMarkers.S3_VERBOSE, "Returning cached content for file: {}", path);
213221
return new ByteArrayInputStream(cachedContent);
214222
}
215-
223+
216224
// Get file size to determine strategy
217225
Long cachedSize = fileSizeCache.get(path);
218226
long fileSize = cachedSize != null ? cachedSize : getFileSizeOnS3(path);
219-
227+
220228
if (cachedSize == null) {
221229
fileSizeCache.put(path, fileSize);
222230
}
223-
231+
224232
String fullFilePath = fullPath(path);
225-
233+
226234
if (fileSize <= STREAMING_THRESHOLD) {
227235
// Cache small files for future use
228236
log.debug(LogMarkers.S3_VERBOSE, "Caching small file: {} ({} bytes)", path, fileSize);
229237
String content = s3Client.getObjectAsString(bucketName, fullFilePath);
230238
byte[] contentBytes = content.getBytes();
231-
239+
232240
// Implement simple cache size management
233241
if (smallFileCache.size() >= MAX_CACHE_SIZE) {
234242
// Remove oldest entry (simple FIFO)
235-
String oldestKey = smallFileCache.keySet().iterator().next();
243+
String oldestKey = smallFileCache
244+
.keySet()
245+
.iterator()
246+
.next();
236247
smallFileCache.remove(oldestKey);
237248
}
238-
249+
239250
smallFileCache.put(path, contentBytes);
240251
return new ByteArrayInputStream(contentBytes);
241252
} else {
@@ -274,7 +285,10 @@ public void clearCaches() {
274285
}
275286

276287
/**
277-
* Get cache statistics for monitoring.
288+
* Retrieves statistics about various internal caches used in the class.
289+
*
290+
* @return A map where the keys are the cache names (e.g., "fileExistsCache",
291+
* "directoryListCache", etc.) and the values are the respective sizes of these caches.
278292
*/
279293
public Map<String, Integer> getCacheStats() {
280294
return Map.of(
@@ -305,7 +319,9 @@ private boolean checkFileExistsOnS3(String path) {
305319

306320
private long getFileSizeOnS3(String path) {
307321
try {
308-
return s3Client.getObjectMetadata(bucketName, fullPath(path)).getContentLength();
322+
return s3Client
323+
.getObjectMetadata(bucketName, fullPath(path))
324+
.getContentLength();
309325
} catch (AmazonServiceException e) {
310326
log.debug("Failed to get file size for path: {}", path, e);
311327
return 0;
@@ -316,22 +332,25 @@ private List<String> listFilesOnS3(String directoryPath) {
316332
try {
317333
List<String> allKeys = new ArrayList<>();
318334
String prefix = fullPath(directoryPath);
319-
335+
320336
ListObjectsRequest request = new ListObjectsRequest()
321337
.withBucketName(bucketName)
322338
.withPrefix(prefix)
323339
.withMaxKeys(1000);
324-
340+
325341
ObjectListing listing;
326342
do {
327343
listing = s3Client.listObjects(request);
328-
allKeys.addAll(listing.getObjectSummaries().stream()
344+
allKeys.addAll(listing
345+
.getObjectSummaries()
346+
.stream()
329347
.map(S3ObjectSummary::getKey)
330348
.collect(Collectors.toList()));
331349
request.setMarker(listing.getNextMarker());
332350
} while (listing.isTruncated());
333-
334-
log.debug(LogMarkers.S3_VERBOSE, "Listed {} files in directory: {}", allKeys.size(), directoryPath);
351+
352+
log.debug(LogMarkers.S3_VERBOSE, "Listed {} files in directory: {}", allKeys.size(),
353+
directoryPath);
335354
return allKeys;
336355
} catch (AmazonServiceException e) {
337356
log.debug("Failed to list files in directory: {}", directoryPath, e);

0 commit comments

Comments
 (0)