Write all logs to disk

This commit is contained in:
Christoph Hagen 2022-12-04 23:10:44 +01:00
parent 956cfb52c4
commit a8b328efce
7 changed files with 159 additions and 82 deletions

View File

@ -3,7 +3,7 @@ import Foundation
struct FileData { struct FileData {
///The files marked as expected, i.e. they exist after the generation is completed. (`key`: file path, `value`: the file providing the link) ///The files marked as expected, i.e. they exist after the generation is completed. (`key`: file path, `value`: the file providing the link)
var expected: [String : String] = [:] var expected: [String : String] = [:]
/// All files which should be copied to the output folder (`key`: The file path, `value`: The source requiring the file) /// All files which should be copied to the output folder (`key`: The file path, `value`: The source requiring the file)
var toCopy: [String : String] = [:] var toCopy: [String : String] = [:]

View File

@ -157,9 +157,10 @@ final class FileGenerator {
command = "cleancss \(url.path) -o \(tempFile.path)" command = "cleancss \(url.path) -o \(tempFile.path)"
} }
try? tempFile.delete() defer {
didMinifyFile()
defer { didMinifyFile() } try? tempFile.delete()
}
let output: String let output: String
do { do {
@ -256,4 +257,30 @@ final class FileGenerator {
} }
return false return false
} }
func writeResults(to file: URL) {
var lines: [String] = []
func add<S>(_ name: String, _ property: S, convert: (S.Element) -> String) where S: Sequence {
let elements = property.map { " " + convert($0) }.sorted()
guard !elements.isEmpty else {
return
}
lines.append("\(name):")
lines.append(contentsOf: elements)
}
add("Unreadable files", unreadableFiles) { "\($0.key) (required by \($0.value.source)): \($0.value.message)" }
add("Unwritable files", unwritableFiles) { "\($0.key) (required by \($0.value.source)): \($0.value.message)" }
add("Failed minifications", failedMinifications) { "\($0.file) (required by \($0.source)): \($0.message)" }
add("Missing files", missingFiles) { "\($0.key) (required by \($0.value))" }
add("Copied files", copiedFiles) { $0 }
add("Minified files", minifiedFiles) { $0 }
add("Expected files", files.expected) { "\($0.key) (required by \($0.value))" }
let data = lines.joined(separator: "\n").data(using: .utf8)!
do {
try data.createFolderAndWrite(to: file)
} catch {
print(" Failed to save log: \(error)")
}
}
} }

View File

@ -441,7 +441,7 @@ final class GenerationResultsHandler {
print("") print("")
} }
func writeResultsToFile(file: URL) throws { func writeResults(to file: URL) {
var lines: [String] = [] var lines: [String] = []
func add<S>(_ name: String, _ property: S, convert: (S.Element) -> String) where S: Sequence { func add<S>(_ name: String, _ property: S, convert: (S.Element) -> String) where S: Sequence {
let elements = property.map { " " + convert($0) }.sorted() let elements = property.map { " " + convert($0) }.sorted()
@ -459,9 +459,12 @@ final class GenerationResultsHandler {
add("Drafts", draftPages) { $0 } add("Drafts", draftPages) { $0 }
add("Empty pages", emptyPages) { "\($0.key) (from \($0.value))" } add("Empty pages", emptyPages) { "\($0.key) (from \($0.value))" }
add("Generated files", generatedFiles) { $0 } add("Generated files", generatedFiles) { $0 }
let data = lines.joined(separator: "\n").data(using: .utf8)! let data = lines.joined(separator: "\n").data(using: .utf8)!
try data.createFolderAndWrite(to: file) do {
try data.createFolderAndWrite(to: file)
} catch {
print(" Failed to save log: \(error)")
}
} }
} }

View File

@ -24,18 +24,24 @@ final class ImageGenerator {
/** /**
All warnings produced for images during generation All warnings produced for images during generation
*/ */
private var imageWarnings: Set<String> = [] private var imageWarnings: [String]
/** /// The images which could not be found, but are required for the site (`key`: image path, `value`: source element path)
All images modified or created during this generator run. private var missingImages: [String : String]
*/
/// Images which could not be read (`key`: file path relative to content, `value`: source element path)
private var unreadableImages: [String : String]
private var unhandledImages: [String: String] = [:]
/// All images modified or created during this generator run.
private var generatedImages: Set<String> = [] private var generatedImages: Set<String> = []
/** /// The images optimized by ImageOptim
The images optimized by ImageOptim
*/
private var optimizedImages: Set<String> = [] private var optimizedImages: Set<String> = []
private var failedImages: [(path: String, message: String)] = []
private var numberOfGeneratedImages = 0 private var numberOfGeneratedImages = 0
private let numberOfTotalImages: Int private let numberOfTotalImages: Int
@ -71,6 +77,9 @@ final class ImageGenerator {
self.images = images self.images = images
self.numberOfTotalImages = images.jobs.reduce(0) { $0 + $1.value.count } self.numberOfTotalImages = images.jobs.reduce(0) { $0 + $1.value.count }
+ images.multiJobs.count * 2 + images.multiJobs.count * 2
self.imageWarnings = images.warnings
self.missingImages = images.missing
self.unreadableImages = images.unreadable
} }
func generateImages() { func generateImages() {
@ -81,15 +90,10 @@ final class ImageGenerator {
} }
notes.append("\(count) \(name)") notes.append("\(count) \(name)")
} }
addIfNotZero(images.missing.count, "missing images")
addIfNotZero(images.unreadable.count, "unreadable images")
print(" Changed sources: \(jobs.count)/\(images.jobs.count)") print(" Changed sources: \(jobs.count)/\(images.jobs.count)")
print(" Total images: \(numberOfTotalImages) (\(numberOfTotalImages - imageReader.numberOfFilesAccessed) versions)") print(" Total images: \(numberOfTotalImages) (\(numberOfTotalImages - imageReader.numberOfFilesAccessed) versions)")
print(" Warnings: \(images.warnings.count)")
if !notes.isEmpty {
print(" Notes: " + notes.joined(separator: ", "))
}
for (source, jobs) in jobs { for (source, jobs) in jobs {
create(images: jobs, from: source) create(images: jobs, from: source)
} }
@ -99,11 +103,18 @@ final class ImageGenerator {
print(" Generated images: \(numberOfGeneratedImages)/\(numberImagesToCreate)") print(" Generated images: \(numberOfGeneratedImages)/\(numberImagesToCreate)")
optimizeImages() optimizeImages()
print(" Optimized images: \(numberOfOptimizedImages)/\(numberOfImagesToOptimize)") print(" Optimized images: \(numberOfOptimizedImages)/\(numberOfImagesToOptimize)")
addIfNotZero(missingImages.count, "missing images")
addIfNotZero(unreadableImages.count, "unreadable images")
print(" Warnings: \(imageWarnings.count)")
if !notes.isEmpty {
print(" Notes: " + notes.joined(separator: ", "))
}
} }
private func create(images: [ImageJob], from source: String) { private func create(images: [ImageJob], from source: String) {
guard let image = imageReader.getImage(atPath: source) else { guard let image = imageReader.getImage(atPath: source) else {
// TODO: Add to failed images unreadableImages[source] = images.first!.destination
didGenerateImage(count: images.count) didGenerateImage(count: images.count)
return return
} }
@ -137,7 +148,7 @@ final class ImageGenerator {
let destinationExtension = destinationUrl.pathExtension.lowercased() let destinationExtension = destinationUrl.pathExtension.lowercased()
guard let type = ImageType(fileExtension: destinationExtension)?.fileType else { guard let type = ImageType(fileExtension: destinationExtension)?.fileType else {
addWarning("Invalid image extension \(destinationExtension)", job: job) unhandledImages[source] = job.destination
return return
} }
@ -168,25 +179,20 @@ final class ImageGenerator {
// Get NSData, and save it // Get NSData, and save it
guard let data = rep.representation(using: type, properties: [.compressionFactor: NSNumber(value: job.quality)]) else { guard let data = rep.representation(using: type, properties: [.compressionFactor: NSNumber(value: job.quality)]) else {
addWarning("Failed to get data", job: job) markImageAsFailed(source, error: "Failed to get data")
return return
} }
do { do {
try data.createFolderAndWrite(to: destinationUrl) try data.createFolderAndWrite(to: destinationUrl)
} catch { } catch {
addWarning("Failed to write image (\(error))", job: job) markImageAsFailed(job.destination, error: "Failed to write image (\(error))")
return return
} }
generatedImages.insert(job.destination) generatedImages.insert(job.destination)
} }
private func addWarning(_ message: String, destination: String, path: String) { private func markImageAsFailed(_ source: String, error: String) {
let warning = " \(destination): \(message) required by \(path)" failedImages.append((source, error))
imageWarnings.insert(warning)
}
private func addWarning(_ message: String, job: ImageJob) {
addWarning(message, destination: job.destination, path: job.path)
} }
private func createMultiImages(from source: String, path: String) { private func createMultiImages(from source: String, path: String) {
@ -198,7 +204,7 @@ final class ImageGenerator {
let sourceUrl = output.appendingPathComponent(source) let sourceUrl = output.appendingPathComponent(source)
let sourcePath = sourceUrl.path let sourcePath = sourceUrl.path
guard sourceUrl.exists else { guard sourceUrl.exists else {
addWarning("No image at path \(sourcePath)", destination: source, path: path) missingImages[source] = path
didGenerateImage(count: 2) didGenerateImage(count: 2)
return return
} }
@ -222,7 +228,7 @@ final class ImageGenerator {
do { do {
_ = try safeShell(command) _ = try safeShell(command)
} catch { } catch {
addWarning("Failed to create AVIF image", destination: destination, path: destination) markImageAsFailed(destination, error: "Failed to create AVIF image")
} }
} }
@ -231,7 +237,7 @@ final class ImageGenerator {
do { do {
_ = try safeShell(command) _ = try safeShell(command)
} catch { } catch {
addWarning("Failed to create WEBP image", destination: destination, path: destination) markImageAsFailed(destination, error: "Failed to create WEBP image")
} }
} }
@ -240,7 +246,7 @@ final class ImageGenerator {
do { do {
_ = try safeShell(command) _ = try safeShell(command)
} catch { } catch {
addWarning("Failed to compress image", destination: destination, path: destination) markImageAsFailed(destination, error: "Failed to compress image")
} }
} }
@ -264,7 +270,9 @@ final class ImageGenerator {
_ = try safeShell(command) _ = try safeShell(command)
return true return true
} catch { } catch {
addWarning("Failed to optimize images", destination: "", path: "") for image in batch {
markImageAsFailed(image, error: "Failed to optimize image")
}
return false return false
} }
} }
@ -282,4 +290,29 @@ final class ImageGenerator {
print(" Optimized images: \(numberOfOptimizedImages)/\(numberOfImagesToOptimize) \r", terminator: "") print(" Optimized images: \(numberOfOptimizedImages)/\(numberOfImagesToOptimize) \r", terminator: "")
fflush(stdout) fflush(stdout)
} }
func writeResults(to file: URL) {
var lines: [String] = []
func add<S>(_ name: String, _ property: S, convert: (S.Element) -> String) where S: Sequence {
let elements = property.map { " " + convert($0) }.sorted()
guard !elements.isEmpty else {
return
}
lines.append("\(name):")
lines.append(contentsOf: elements)
}
add("Missing images", missingImages) { "\($0.key) (required by \($0.value))" }
add("Unreadable images", unreadableImages) { "\($0.key) (required by \($0.value))" }
add("Failed images", failedImages) { "\($0.path): \($0.message)" }
add("Unhandled images", unhandledImages) { "\($0.value) (from \($0.key))" }
add("Warnings", imageWarnings) { $0 }
add("Generated images", generatedImages) { $0 }
add("Optimized images", optimizedImages) { $0 }
let data = lines.joined(separator: "\n").data(using: .utf8)!
do {
try data.createFolderAndWrite(to: file)
} catch {
print(" Failed to save log: \(error)")
}
}
} }

View File

@ -4,8 +4,6 @@ final class MetadataInfoLogger {
private let input: URL private let input: URL
private let runFolder: URL
private var numberOfMetadataFiles = 0 private var numberOfMetadataFiles = 0
private var unusedProperties: [(name: String, source: String)] = [] private var unusedProperties: [(name: String, source: String)] = []
@ -22,13 +20,8 @@ final class MetadataInfoLogger {
private var errors: [(source: String, message: String)] = [] private var errors: [(source: String, message: String)] = []
private var logFile: URL { init(input: URL) {
runFolder.appendingPathComponent("Metadata issues.txt")
}
init(input: URL, runFolder: URL) {
self.input = input self.input = input
self.runFolder = runFolder
} }
/** /**
@ -131,7 +124,7 @@ final class MetadataInfoLogger {
// MARK: Printing // MARK: Printing
private func printMetadataScanUpdate() { private func printMetadataScanUpdate() {
print(String(format: " Pages found: %4d \r", numberOfMetadataFiles), terminator: "") print(" Pages found: \(numberOfMetadataFiles) \r", terminator: "")
} }
func printMetadataScanOverview(languages: Int) { func printMetadataScanOverview(languages: Int) {
@ -157,31 +150,29 @@ final class MetadataInfoLogger {
} }
} }
func writeResultsToFile() throws { func writeResults(to file: URL) {
var lines: [String] = [] var lines: [String] = []
if !errors.isEmpty { func add<S>(_ name: String, _ property: S, convert: (S.Element) -> String) where S: Sequence {
lines += ["Errors:"] + errors.map { "\($0.source): \($0.message)" }.sorted() let elements = property.map { " " + convert($0) }.sorted()
} guard !elements.isEmpty else {
if !warnings.isEmpty { return
lines += ["Warnings:"] + warnings.map { "\($0.source): \($0.message)" }.sorted() }
} lines.append("\(name):")
if !unreadableMetadata.isEmpty { lines.append(contentsOf: elements)
lines += ["Unreadable files:"] + unreadableMetadata.map { "\($0.source): \($0.error)" }.sorted()
}
if !unusedProperties.isEmpty {
lines += ["Unused properties:"] + unusedProperties.map { "\($0.source): \($0.name)" }.sorted()
}
if !invalidProperties.isEmpty {
lines += ["Invalid properties:"] + invalidProperties.map { "\($0.source): \($0.name) (\($0.reason))" }.sorted()
}
if !unknownProperties.isEmpty {
lines += ["Unknown properties:"] + unknownProperties.map { "\($0.source): \($0.name)" }.sorted()
}
if !missingProperties.isEmpty {
lines += ["Missing properties:"] + missingProperties.map { "\($0.source): \($0.name)" }.sorted()
} }
add("Errors", errors) { "\($0.source): \($0.message)" }
add("Warnings", warnings) { "\($0.source): \($0.message)" }
add("Unreadable files", unreadableMetadata) { "\($0.source): \($0.error)" }
add("Unused properties", unusedProperties) { "\($0.source): \($0.name)" }
add("Invalid properties", invalidProperties) { "\($0.source): \($0.name) (\($0.reason))" }
add("Unknown properties", unknownProperties) { "\($0.source): \($0.name)" }
add("Missing properties", missingProperties) { "\($0.source): \($0.name)" }
let data = lines.joined(separator: "\n").data(using: .utf8)! let data = lines.joined(separator: "\n").data(using: .utf8)!
try data.createFolderAndWrite(to: logFile) do {
try data.createFolderAndWrite(to: file)
} catch {
print(" Failed to save log: \(error)")
}
} }
} }

View File

@ -22,8 +22,13 @@ extension Template {
} }
init(from url: URL, results: GenerationResultsHandler) throws { init(from url: URL, results: GenerationResultsHandler) throws {
let raw = try String(contentsOf: url) do {
self.init(raw: raw, results: results) let raw = try String(contentsOf: url)
self.init(raw: raw, results: results)
} catch {
results.warning("Failed to load: \(error)", source: "Template \(url.lastPathComponent)")
throw error
}
} }
@discardableResult @discardableResult

View File

@ -36,22 +36,29 @@ private func loadSiteData(in folder: URL, runFolder: URL) throws -> (root: Eleme
print("--- SOURCE FILES -----------------------------------") print("--- SOURCE FILES -----------------------------------")
print(" ") print(" ")
let log = MetadataInfoLogger(input: folder, runFolder: runFolder) let log = MetadataInfoLogger(input: folder)
let root = Element(atRoot: folder, log: log) let root = Element(atRoot: folder, log: log)
log.printMetadataScanOverview(languages: root?.languages.count ?? 0)
print(" ") let file = runFolder.appendingPathComponent("metadata.txt")
try log.writeResultsToFile() defer {
log.writeResults(to: file)
print(" ")
}
guard let root else { guard let root else {
log.printMetadataScanOverview(languages: 0)
print(" Error: No site root loaded, aborting generation")
return nil return nil
} }
let ids = root.getContainedIds(log: log) let ids = root.getContainedIds(log: log)
log.printMetadataScanOverview(languages: root.languages.count)
return (root, ids) return (root, ids)
} }
private func generatePages(from root: Element, configuration: Configuration, fileUpdates: FileUpdateChecker, ids: [String: String], pageCount: Int, runFolder: URL) throws -> (ImageData, FileData) { private func generatePages(from root: Element, configuration: Configuration, fileUpdates: FileUpdateChecker, ids: [String: String], runFolder: URL) -> (ImageData, FileData)? {
print("--- GENERATION -------------------------------------") print("--- GENERATION -------------------------------------")
print(" ") print(" ")
let pageCount = ids.count * root.languages.count
let results = GenerationResultsHandler( let results = GenerationResultsHandler(
in: configuration.contentDirectory, in: configuration.contentDirectory,
to: configuration.outputDirectory, to: configuration.outputDirectory,
@ -60,11 +67,22 @@ private func generatePages(from root: Element, configuration: Configuration, fil
pagePaths: ids, pagePaths: ids,
pageCount: pageCount) pageCount: pageCount)
let siteGenerator = try SiteGenerator(results: results) defer { results.printOverview() }
let siteGenerator: SiteGenerator
do {
siteGenerator = try SiteGenerator(results: results)
} catch {
return nil
}
siteGenerator.generate(site: root) siteGenerator.generate(site: root)
results.printOverview()
let url = runFolder.appendingPathComponent("files.txt") let url = runFolder.appendingPathComponent("pages.txt")
try results.writeResultsToFile(file: url) results.writeResults(to: url)
if let error = fileUpdates.writeDetectedFileChanges(to: runFolder) {
print(" Hashes not saved: \(error)")
}
return (results.images, results.files) return (results.images, results.files)
} }
@ -78,6 +96,8 @@ private func generateImages(_ images: ImageData, configuration: Configuration, r
reader: reader, images: images) reader: reader, images: images)
generator.generateImages() generator.generateImages()
print(" ") print(" ")
let file = runFolder.appendingPathComponent("images.txt")
generator.writeResults(to: file)
} }
private func copyFiles(files: FileData, configuration: Configuration, runFolder: URL) { private func copyFiles(files: FileData, configuration: Configuration, runFolder: URL) {
@ -131,11 +151,9 @@ private func generate(configPath: String) throws {
} }
// 3. Generate pages // 3. Generate pages
let pageCount = ids.count * siteRoot.languages.count
let (images, files) = try generatePages(from: siteRoot, configuration: configuration, fileUpdates: fileUpdates, ids: ids, pageCount: pageCount, runFolder: runFolder)
if let error = fileUpdates.writeDetectedFileChanges(to: runFolder) { guard let (images, files) = generatePages(from: siteRoot, configuration: configuration, fileUpdates: fileUpdates, ids: ids, runFolder: runFolder) else {
print(error) return
} }
// 4. Generate images // 4. Generate images