diff --git a/app/channels/onlylogs/logs_channel.rb b/app/channels/onlylogs/logs_channel.rb index efc24d6..e793872 100644 --- a/app/channels/onlylogs/logs_channel.rb +++ b/app/channels/onlylogs/logs_channel.rb @@ -39,19 +39,18 @@ def initialize_watcher(data) return end - cursor_position = data["cursor_position"] || 0 filter = data["filter"].presence mode = data["mode"] || "live" regexp_mode = data["regexp_mode"] == true || data["regexp_mode"] == "true" start_position = data["start_position"]&.to_i || 0 end_position = data["end_position"]&.to_i - if mode == "search" - # For search mode, read the entire file with filter and send all matching lines - read_entire_file_with_filter(file_path, filter, regexp_mode, start_position, end_position) + if mode == "static" + # Read a bounded snapshot once, then finish. + read_static(file_path, filter, regexp_mode, start_position, end_position) else - # For live mode, start the watcher - start_log_watcher(file_path, cursor_position, filter, regexp_mode) + # Follow the tail of the file indefinitely. + start_log_watcher(file_path, live_tail_position(file_path), filter, regexp_mode) end end @@ -75,6 +74,14 @@ def cleanup_existing_operations stop_log_watcher end + # Bytes from the end of the file to show when starting a live tail without + # an explicit cursor (matches the default whole-file live-mode page load). + LIVE_TAIL_BYTES = 10_000 + + def live_tail_position(file_path) + [::File.size(file_path) - LIVE_TAIL_BYTES, 0].max + end + def start_log_watcher(file_path, cursor_position, filter = nil, regexp_mode = false) return if @log_watcher_running @@ -90,6 +97,7 @@ def start_log_watcher(file_path, cursor_position, filter = nil, regexp_mode = fa @log_watcher_thread = Thread.new do Rails.logger.silence(Logger::ERROR) do + current_byte_offset = cursor_position @log_file.watch do |new_lines| break unless @log_watcher_running @@ -102,7 +110,8 @@ def start_log_watcher(file_path, cursor_position, filter = nil, regexp_mode = fa # next # end - lines_to_send << render_log_line(log_line) + lines_to_send << render_log_line(log_line, byte_offset: current_byte_offset) + current_byte_offset += log_line.bytesize end if lines_to_send.any? @@ -143,11 +152,11 @@ def stop_log_watcher @log_file = nil end - def read_entire_file_with_filter(file_path, filter = nil, regexp_mode = false, start_position = 0, end_position = nil) + def read_static(file_path, filter = nil, regexp_mode = false, start_position = 0, end_position = nil) @log_watcher_running = true @log_file = Onlylogs::File.new(file_path, last_position: 0) - transmit({action: "message", content: "Searching..."}) + transmit({action: "message", content: filter.present? ? "Searching..." : "Loading..."}) @batch_sender = BatchSender.new(self) @batch_sender.start @@ -156,11 +165,15 @@ def read_entire_file_with_filter(file_path, filter = nil, regexp_mode = false, s begin Rails.logger.silence(Logger::ERROR) do - @log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |log_line| + @log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |result| break if @batch_sender.nil? + # Result is now a hash with {byte_offset, content} + byte_offset = result[:byte_offset] + log_line = result[:content] + # Add to batch buffer (sender thread will handle sending) - @batch_sender.add_line(render_log_line(log_line)) + @batch_sender.add_line(render_log_line(log_line, byte_offset: byte_offset, show_expand_button: true)) line_count += 1 end @@ -170,7 +183,7 @@ def read_entire_file_with_filter(file_path, filter = nil, regexp_mode = false, s @batch_sender.stop # Send completion message - if line_count >= Onlylogs.max_line_matches + if Onlylogs.max_line_matches && line_count >= Onlylogs.max_line_matches transmit({action: "finish", content: "Search finished. Search results limit reached."}) else transmit({action: "finish", content: "Search finished."}) @@ -184,8 +197,14 @@ def read_entire_file_with_filter(file_path, filter = nil, regexp_mode = false, s end end - def render_log_line(log_line) - "
#{FilePathParser.parse(AnsiColorParser.parse(ERB::Util.html_escape(log_line)))}"
+ def render_log_line(log_line, byte_offset: nil, show_expand_button: false)
+ parsed = FilePathParser.parse(AnsiColorParser.parse(ERB::Util.html_escape(log_line)))
+
+ {
+ content: parsed,
+ byte_offset: byte_offset,
+ show_expand_button: show_expand_button
+ }
end
end
end
diff --git a/app/controllers/onlylogs/logs_controller.rb b/app/controllers/onlylogs/logs_controller.rb
index b503225..c9e4c2c 100644
--- a/app/controllers/onlylogs/logs_controller.rb
+++ b/app/controllers/onlylogs/logs_controller.rb
@@ -11,7 +11,11 @@ def index
@filter = params[:filter]
@autoscroll = params[:autoscroll] != "false"
@regexp_mode = params[:regexp_mode] == "true"
- @mode = @filter.blank? ? (params[:mode] || "live") : "search" # "live" or "search"
+ @mode = @filter.blank? ? (params[:mode] || "live") : "static" # "live" or "static"
+ @start_position = nil
+ @end_position = nil
+
+ handle_byte_offset if params[:byte_offset].present?
end
def download
@@ -31,6 +35,19 @@ def download
private
+ EXPLORE_WINDOW_BYTES = 10_000
+
+ def handle_byte_offset
+ byte_offset = params[:byte_offset]&.to_i
+ return unless byte_offset.present?
+
+ @start_position = [byte_offset - EXPLORE_WINDOW_BYTES, 0].max
+ @end_position = byte_offset + EXPLORE_WINDOW_BYTES
+ @filter = nil
+ @mode = "static"
+ @autoscroll = false
+ end
+
def selected_log_file_path
return default_log_file_path if params[:log_file_path].blank?
authorized_log_file_path(params[:log_file_path])
diff --git a/app/javascript/onlylogs/controllers/log_streamer_controller.js b/app/javascript/onlylogs/controllers/log_streamer_controller.js
index e5781cc..6add16e 100644
--- a/app/javascript/onlylogs/controllers/log_streamer_controller.js
+++ b/app/javascript/onlylogs/controllers/log_streamer_controller.js
@@ -4,7 +4,8 @@ import { createConsumer } from "@rails/actioncable";
export default class LogStreamerController extends Controller {
static values = {
filePath: { type: String },
- cursorPosition: { type: Number, default: 0 },
+ startPosition: { type: Number, default: 0 },
+ endPosition: { type: Number, default: 0 },
autoScroll: { type: Boolean, default: true },
autoStart: { type: Boolean, default: true },
filter: { type: String, default: '' },
@@ -12,7 +13,7 @@ export default class LogStreamerController extends Controller {
regexpMode: { type: Boolean, default: false }
};
- static targets = ["logLines", "filterInput", "results", "liveMode", "message", "regexpMode", "websocketStatus", "stopButton", "clearButton", "autoscroll"];
+ static targets = ["logLines", "filterInput", "byteOffsetInput", "results", "liveMode", "message", "regexpMode", "websocketStatus", "stopButton", "clearButton", "autoscroll"];
connect() {
this.consumer = createConsumer();
@@ -21,6 +22,7 @@ export default class LogStreamerController extends Controller {
this.isRunning = false;
this.reconnectTimeout = null;
this.isSearchFinished = true;
+ this.contextLineHighlighted = false;
// Initialize clusterize
this.clusterize = null;
@@ -100,7 +102,7 @@ export default class LogStreamerController extends Controller {
if (this.isLiveMode()) {
this.liveModeTarget.checked = false;
- this.modeValue = 'search';
+ this.modeValue = 'static';
this.updateLiveModeState();
this.stop();
}
@@ -116,9 +118,9 @@ export default class LogStreamerController extends Controller {
}
toggleLiveMode() {
- // this condition looks revered, but the value here has been changed already. so the live mode has been enabled.
if (this.isLiveMode()) {
this.modeValue = 'live';
+ this.clearFilter();
this.updateLiveModeState();
if (!this.isRunning) {
this.start();
@@ -132,20 +134,25 @@ export default class LogStreamerController extends Controller {
applyFilter() {
const filterValue = this.filterInputTarget.value;
- // If filter is applied, disable live mode
+ // A filter switches to static mode; an empty filter goes back to live.
if (filterValue && filterValue.trim() !== '') {
this.liveModeTarget.checked = false;
- this.modeValue = 'search';
+ this.modeValue = 'static';
} else {
- // If no filter, enable live mode
this.liveModeTarget.checked = true;
this.modeValue = 'live';
}
+ // Applying a filter searches the whole file, so drop any explore window.
+ this.startPositionValue = 0;
+ this.endPositionValue = 0;
+
// Update visual state
this.updateLiveModeState();
this.updateStopButtonVisibility();
this.#updateUrlParam('filter', filterValue || null);
+ this.#updateUrlParam('byte_offset', null);
+ this.byteOffsetInputTarget.value = '';
// Use the global debounced reconnection (300ms delay)
this.reconnectWithNewMode();
@@ -178,19 +185,29 @@ export default class LogStreamerController extends Controller {
}
clearFilter() {
- // Clear the filter input
+ // Clear filter and explore window to go back to pure live mode
this.filterInputTarget.value = '';
+ this.byteOffsetInputTarget.value = '';
+ this.modeValue = 'live';
+ this.startPositionValue = 0;
+ this.endPositionValue = 0;
+ this.contextLineHighlighted = false;
+
+ // Remove any highlighting
+ this.logLinesTarget.querySelectorAll('.highlighted-context-line').forEach(el => {
+ el.classList.remove('highlighted-context-line');
+ });
- // Re-enable live mode
+ // Re-enable live mode checkbox
this.liveModeTarget.checked = true;
- this.modeValue = 'live';
// Update visual state
this.updateLiveModeState();
this.updateStopButtonVisibility();
- // Update URL with cleared filter
- this.#updateUrlParam('filter');
+ // Update URL with cleared params
+ this.#updateUrlParam('filter', null);
+ this.#updateUrlParam('byte_offset', null);
// Reconnect with cleared filter and live mode
this.reconnectWithNewMode();
@@ -206,21 +223,98 @@ export default class LogStreamerController extends Controller {
this.#hideMessage();
}
+ jumpToByteOffset(e) {
+ if (e instanceof KeyboardEvent && e.key !== 'Enter') return;
+
+ const byteOffset = this.byteOffsetInputTarget.value;
+ if (!byteOffset || isNaN(byteOffset)) return;
+
+ const params = new URLSearchParams(window.location.search);
+ params.set('byte_offset', byteOffset);
+ params.delete('filter');
+ window.location.href = `${window.location.pathname}?${params.toString()}`;
+ }
+
+ handleExpandClick(e) {
+ const btn = e.target.closest('.onlylogs-expand-btn');
+ if (!btn) return;
+
+ const byteOffset = btn.getAttribute('data-byte-offset');
+ if (!byteOffset) return;
+
+ const params = new URLSearchParams(window.location.search);
+ params.set('byte_offset', byteOffset);
+ params.delete('filter');
+ params.set('autoscroll', 'false');
+ params.delete('regexp_mode');
+
+ window.location.href = `${window.location.pathname}?${params.toString()}`;
+ }
+
+ #highlightContextLine() {
+ const target = Number(new URLSearchParams(window.location.search).get('byte_offset'));
+ if (Number.isNaN(target)) return;
+
+ this.#applyContextLineHighlight(target);
+
+ // Only scroll on initial highlight
+ if (!this.contextLineHighlighted) {
+ const closestPre = [...this.logLinesTarget.querySelectorAll('pre[data-byte-offset]')]
+ .find(pre => Number(pre.dataset.byteOffset) === target ||
+ Math.abs(Number(pre.dataset.byteOffset) - target) < 1000);
+ if (closestPre) {
+ this.#scrollVerticallyToCenter(closestPre);
+ }
+ }
+ }
+
+ #scrollVerticallyToCenter(element) {
+ const container = this.logLinesTarget;
+ const containerRect = container.getBoundingClientRect();
+ const elementRect = element.getBoundingClientRect();
+ const delta = (elementRect.top - containerRect.top) - (container.clientHeight / 2) + (elementRect.height / 2);
+ container.scrollBy({ top: delta, behavior: 'smooth' });
+ }
+
+ #applyContextLineHighlight(target) {
+ const closestPre = [...this.logLinesTarget.querySelectorAll('pre[data-byte-offset]')]
+ .reduce((closest, pre) => {
+ const distance = Math.abs(Number(pre.dataset.byteOffset) - target);
+ return !closest || distance < closest.distance ? { pre, distance } : closest;
+ }, null)?.pre;
+
+ if (!closestPre) return;
+
+ const row = this.#rowElement(closestPre);
+ [row.previousElementSibling, row, row.nextElementSibling]
+ .filter(Boolean)
+ .forEach(line => line.classList.add('highlighted-context-line'));
+ }
+
+ // A row is either a bare or an expand-button wrapperdirectly + // under the clusterize content area. Walk up to that top-level element so the + // highlight covers the whole line, including the "+" toggle. + #rowElement(element) { + let node = element; + while (node.parentElement && !node.parentElement.classList.contains('clusterize-content')) { + node = node.parentElement; + } + return node; + } + updateLiveModeState() { const liveModeLabel = this.liveModeTarget.closest('label'); const hasFilter = this.filterInputTarget.value && this.filterInputTarget.value.trim() !== ''; if (hasFilter) { - liveModeLabel.classList.add('live-mode-sticky'); this.liveModeTarget.disabled = true; } else { - liveModeLabel.classList.remove('live-mode-sticky'); this.liveModeTarget.disabled = false; } } updateStopButtonVisibility() { - const shouldShow = !this.isLiveMode() && this.subscription && this.isRunning && !this.isSearchFinished; + const shouldShow = this.modeValue === 'static' && this.subscription && this.isRunning && !this.isSearchFinished; this.stopButtonTarget.style.display = shouldShow ? 'inline-block' : 'none'; } @@ -261,13 +355,23 @@ export default class LogStreamerController extends Controller { * Handle successful connection */ #handleConnected() { - this.subscription.perform('initialize_watcher', { - cursor_position: this.cursorPositionValue, + const data = { file_path: this.filePathValue, filter: this.filterInputTarget.value, mode: this.modeValue, regexp_mode: this.regexpModeValue - }); + }; + + if (this.modeValue === 'static') { + // A byte-offset explore window reads a bounded range. Without a window + // (start/end), a static read is a whole-file filter search. + if (this.endPositionValue > 0) { + data.start_position = this.startPositionValue; + data.end_position = this.endPositionValue; + } + } + + this.subscription.perform('initialize_watcher', data); this.element.classList.add("log-streamer--connected"); this.element.classList.remove("log-streamer--disconnected", "log-streamer--rejected"); @@ -293,9 +397,20 @@ export default class LogStreamerController extends Controller { try { // Append new lines to clusterize if (lines.length > 0) { - this.clusterize.append(lines); + // Render JSON log lines into HTML strings + const renderedLines = lines.map(line => this.#renderLogLineHtml(line)); + this.clusterize.append(renderedLines); this.#updateResultsDisplay(); this.scroll(); + + // Find and highlight lines around the target byte offset + const params = new URLSearchParams(window.location.search); + if (params.has('byte_offset') && !this.contextLineHighlighted) { + setTimeout(() => { + this.#highlightContextLine(); + this.contextLineHighlighted = true; + }, 100); + } } // Update stop button visibility after processing lines @@ -306,6 +421,19 @@ export default class LogStreamerController extends Controller { } } + #renderLogLineHtml(logLine) { + // logLine is now a JSON object: {content, byte_offset, show_expand_button} + const { content, byte_offset, show_expand_button } = logLine; + + if (byte_offset && show_expand_button) { + return `- <%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll, regexp_mode: @regexp_mode } %> + <%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll, regexp_mode: @regexp_mode, start_position: @start_position, end_position: @end_position, mode: @mode } %> diff --git a/app/views/onlylogs/shared/_log_container.html.erb b/app/views/onlylogs/shared/_log_container.html.erb index 916274d..0a99220 100644 --- a/app/views/onlylogs/shared/_log_container.html.erb +++ b/app/views/onlylogs/shared/_log_container.html.erb @@ -1,11 +1,13 @@ -<%# locals: (log_file_path:, tail: 100, filter: "", autoscroll: true, regexp_mode: false) %> +<%# locals: (log_file_path:, tail: 100, filter: "", autoscroll: true, regexp_mode: false, start_position: nil, end_position: nil, mode: nil) %> <%= render "onlylogs/shared/log_container_styles" %> <% - mode = filter.blank? ? "live" : "search" - cursor_position = mode == "search" ? 0 : [File.size(log_file_path) - (tail * 100), 0].max + mode = local_assigns[:mode] || (filter.blank? ? "live" : "static") + start_position = local_assigns[:start_position] + end_position = local_assigns[:end_position] + byte_offset = request.params[:byte_offset] raise SecurityError, "File path not allowed" unless Onlylogs.file_path_permitted?(log_file_path) @@ -14,7 +16,8 @@`; + } else if (byte_offset) { + return `${content}${content}`; + } else { + return `${content}`; + } + } + #handleMessage(message) { this.#hideMessage(); if (message === '') { @@ -380,7 +508,6 @@ export default class LogStreamerController extends Controller { return { isRunning: this.isRunning, filePath: this.filePathValue, - cursorPosition: this.cursorPositionValue, lineCount: this.clusterize.getRowsAmount(), connected: this.subscription && this.subscription.identifier }; @@ -403,7 +530,15 @@ export default class LogStreamerController extends Controller { // Optional: handle cluster change }, clusterChanged: () => { - // Optional: handle after cluster change + // Re-apply highlighting when cluster changes (for virtual scrolling). + // The byte_offset URL param is the highlight anchor for an explore window. + const params = new URLSearchParams(window.location.search); + if (params.has('byte_offset')) { + const target = Number(params.get('byte_offset')); + if (!Number.isNaN(target)) { + this.#applyContextLineHighlight(target); + } + } }, scrollingProgress: (progress) => { // Optional: handle scrolling progress diff --git a/app/models/onlylogs/grep.rb b/app/models/onlylogs/grep.rb index e9e2711..e7f1eae 100644 --- a/app/models/onlylogs/grep.rb +++ b/app/models/onlylogs/grep.rb @@ -19,16 +19,29 @@ def self.grep(pattern, file_path, start_position: 0, end_position: nil, regexp_m results = [] + # Set up parsing logic based on whether ripgrep includes byte offsets + parse_line = if Onlylogs.ripgrep_enabled? + ->(line) { + parts = line.split(":", 2) + [parts[0].to_i, parts[1] || ""] + } + else + ->(line) { [nil, line] } + end + IO.popen(command_args, err: "/dev/null") do |io| io.each_line do |line| - # Line numbers are no longer outputted by super_grep/super_ripgrep + byte_offset, content = parse_line.call(line.chomp) + # Use String.new to create a copy and prevent memory retention from IO buffers - content = String.new(line.chomp, encoding: Encoding::UTF_8).scrub + content = String.new(content, encoding: Encoding::UTF_8).scrub + + result = {byte_offset: byte_offset, content: content} if block_given? - yield content + yield result else - results << content + results << result end end end diff --git a/app/views/onlylogs/logs/index.html.erb b/app/views/onlylogs/logs/index.html.erb index 6cc8564..3bf5498 100644 --- a/app/views/onlylogs/logs/index.html.erb +++ b/app/views/onlylogs/logs/index.html.erb @@ -36,5 +36,5 @@ <% end %> <% end %>data-log-streamer-start-position-value="<%= start_position %>"<% end %> + <% if end_position.present? %>data-log-streamer-end-position-value="<%= end_position %>"<% end %> data-log-streamer-filter-value="<%= filter %>" data-log-streamer-auto-scroll-value="<%= autoscroll %>" data-log-streamer-regexp-mode-value="<%= regexp_mode %>" @@ -75,6 +78,9 @@++ Results: 0 +-- Results: 0 -+ <% if Onlylogs.ripgrep_enabled? %> ++ ++ <% end %> <% unless Onlylogs.ripgrep_enabled? %> ⚠️ <% end %> diff --git a/app/views/onlylogs/shared/_log_container_styles.html.erb b/app/views/onlylogs/shared/_log_container_styles.html.erb index 37a2cf3..40fb36d 100644 --- a/app/views/onlylogs/shared/_log_container_styles.html.erb +++ b/app/views/onlylogs/shared/_log_container_styles.html.erb @@ -24,8 +24,25 @@ } } + .onlylogs-expand-btn { + cursor: pointer; + padding: 0 0.5rem; + margin-right: 0.25rem; + background: none; + border: none; + color: #666; + font-weight: bold; + font-size: 1.1em; + line-height: 1; + + &:hover { + color: #333; + } + } + .clusterize-content { outline: 0; + min-width: max-content; } .color-success { @@ -74,8 +91,15 @@ pre { margin: 0 !important; - padding: 0.2rem; + padding: 0 0.2rem; + line-height: var(--onlylogs-line-height, 1.6rem); + min-height: var(--onlylogs-line-height, 1.6rem); word-break: break-word; /* allow breaking long tokens like UUIDs/SQL */ + white-space: pre; + } + + .highlighted-context-line { + background-color: rgba(255, 193, 7, 0.3); } a { @@ -121,15 +145,9 @@ align-items: center; gap: 1.5rem; - .live-mode-sticky { - opacity: 0.7; - cursor: not-allowed; - input[type="checkbox"] { - cursor: not-allowed; - } - } + } .clear-filter-button { position: absolute; right: 0.25rem; diff --git a/bin/super_ripgrep b/bin/super_ripgrep index 8dac08a..2774698 100755 --- a/bin/super_ripgrep +++ b/bin/super_ripgrep @@ -56,7 +56,7 @@ actual_color_regex='\x1b\[[0-9;]*m' query_regex="${query_regex//$placeholder/$actual_color_regex}" # Build ripgrep command -rg_cmd="rg --color=never --no-filename" +rg_cmd="rg --color=never --no-filename --byte-offset" [ -n "$max_matches" ] && rg_cmd="$rg_cmd --max-count=$max_matches" # Handle byte range if specified diff --git a/test/controllers/onlylogs/logs_controller_test.rb b/test/controllers/onlylogs/logs_controller_test.rb index 2432585..1d84426 100644 --- a/test/controllers/onlylogs/logs_controller_test.rb +++ b/test/controllers/onlylogs/logs_controller_test.rb @@ -115,5 +115,50 @@ class LogsControllerTest < ActionDispatch::IntegrationTest assert_select "[data-log-streamer-auto-scroll-value='false']" assert_select "[data-log-streamer-regexp-mode-value='true']" end + + test "index handles byte_offset param and sets search mode with byteoffset type" do + get "/onlylogs", params: {byte_offset: "50000"} + assert_response :success + # Should be in search mode with byteoffset search type + assert_select "[data-log-streamer-mode-value='search']" + assert_select "[data-log-streamer-search-type-value='byteoffset']" + assert_select "[data-log-streamer-filter-value='']" + assert_select "[data-log-streamer-auto-scroll-value='false']" + end + + test "index calculates cursor_position from byte_offset" do + get "/onlylogs", params: {byte_offset: "50000"} + assert_response :success + # cursor_position should be byte_offset - 10000 = 40000 + assert_select "[data-log-streamer-cursor-position-value='40000']" + end + + test "index sets end_position from byte_offset" do + get "/onlylogs", params: {byte_offset: "50000"} + assert_response :success + # end_position should be byte_offset + 10000 = 60000 + assert_select "[data-log-streamer-end-position-value='60000']" + end + + test "index sets search_type to filter when filter param is present" do + get "/onlylogs", params: {filter: "warning"} + assert_response :success + assert_select "[data-log-streamer-mode-value='search']" + assert_select "[data-log-streamer-search-type-value='filter']" + end + + test "index preserves autoscroll and regexp_mode params when not exploring" do + get "/onlylogs", params: {filter: "warning", autoscroll: "true", regexp_mode: "true"} + assert_response :success + assert_select "[data-log-streamer-auto-scroll-value='true']" + assert_select "[data-log-streamer-regexp-mode-value='true']" + end + + test "index byte_offset with cursor_position boundary at 0" do + get "/onlylogs", params: {byte_offset: "5000"} + assert_response :success + # cursor_position should be max(5000 - 10000, 0) = 0 + assert_select "[data-log-streamer-cursor-position-value='0']" + end end end diff --git a/test/models/onlylogs/grep_test.rb b/test/models/onlylogs/grep_test.rb index 7a8254a..1967985 100644 --- a/test/models/onlylogs/grep_test.rb +++ b/test/models/onlylogs/grep_test.rb @@ -28,31 +28,31 @@ def self.test_both_engine_modes(test_name, &block) test_both_engine_modes "it can grep for a simple string in a log file" do |engine_name| lines = Onlylogs::Grep.grep("[DEBUG]", @fixture_path) assert_equal 49, lines.length, "Failed with #{engine_name}" - assert_equal "[DEBUG] Initializing database connection - Line 2", lines.first - assert_equal "[DEBUG] Application metrics - Line 98", lines.last + assert_equal "[DEBUG] Initializing database connection - Line 2", lines.first[:content] + assert_equal "[DEBUG] Application metrics - Line 98", lines.last[:content] end test_both_engine_modes "it can grep a simple string in a log file and yield each returned line" do |engine_name| lines = [] - Onlylogs::Grep.grep("[DEBUG]", @fixture_path) do |content| - lines << content + Onlylogs::Grep.grep("[DEBUG]", @fixture_path) do |result| + lines << result end assert_equal 49, lines.length, "Failed with #{engine_name}" - assert_equal "[DEBUG] Initializing database connection - Line 2", lines.first - assert_equal "[DEBUG] Application metrics - Line 98", lines.last + assert_equal "[DEBUG] Initializing database connection - Line 2", lines.first[:content] + assert_equal "[DEBUG] Application metrics - Line 98", lines.last[:content] end test_both_engine_modes "it returns all INFO lines" do |engine_name| lines = Onlylogs::Grep.grep("[INFO]", @fixture_path) assert_equal 50, lines.length, "Failed with #{engine_name}" - assert_equal "[INFO] Application started - Line 1", lines.first - assert_equal "[INFO] Metrics collected: 150 data points - Line 99", lines.last + assert_equal "[INFO] Application started - Line 1", lines.first[:content] + assert_equal "[INFO] Metrics collected: 150 data points - Line 99", lines.last[:content] end test_both_engine_modes "it can grep a string when the line contains ansi colors" do |engine_name| expected_line = "\e[1m\e[36mActiveRecord::SchemaMigration Load (0.0ms)\e[0m \e[1m\e[34mSELECT ...\e[0m" lines = Onlylogs::Grep.grep("(0.0ms) SELECT", @special_lines_path) - assert_equal [expected_line], lines, "Failed with #{engine_name}" + assert_equal [expected_line], lines.map { |l| l[:content] }, "Failed with #{engine_name}" end test_both_engine_modes "it can grep a string with special regex characters" do |engine_name|