Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Speed up coverage report generation · Issue #636 · TypedDevs/bashunit · GitHub
Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Speed up coverage report generation · Issue #636 · TypedDevs/bashunit · GitHub
Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Speed up coverage report generation · Issue #636 · TypedDevs/bashunit · GitHub
Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Speed up coverage report generation · Issue #636 · TypedDevs/bashunit · GitHub
Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Speed up coverage report generation · Issue #636 · TypedDevs/bashunit · GitHub
Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Speed up coverage report generation · Issue #636 · TypedDevs/bashunit · GitHub
Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Speed up coverage report generation · Issue #636 · TypedDevs/bashunit · GitHub
Skip to content

Speed up coverage report generation #636

Description

@RevanProdigalKnight

As I continue to expand my own project's suite of unit tests I have started to look through the code in bashunit to try and identify pain points. I know I'm penalizing myself by running bashunit on a 15+ year old computer, in Cygwin, on Windows 10 (as opposed to running on proper Linux), as on my PC the test suite takes several minutes to run in serial mode and on the build pipeline agent, the same suite takes under a minute to run.

Even considering all of that, on my PC unit test generation times are... rather long. (I haven't tried introducing it on the build pipeline yet)

$ time ./lib/bashunit tests/* --simple --jobs 4 --coverage --coverage-report-html
bashunit - 0.34.1
# Test output omitted for brevity
Tests: 194 passed, 5 skipped, 2 incomplete, 201 total
Assertions: 1100 passed, 5 skipped, 2 incomplete, 1107 total
Some tests incomplete
Time taken: 9m 14s
Coverage Report
---------------
# Specific file output omitted for brevity
---------------
Total: 1409/1734 (81%)
Coverage report written to: coverage/lcov.info
Coverage HTML report written to: coverage/html/index.html
real 217m36.766s
user 52m52.586s
sys 247m19.326s

Running with --parallel for unlimited testing concurrency instead of a fixed number of jobs does not increase speed significantly, since my PC is so old and only has 6 CPU cores.

As an aside, for some reason my unit tests ran faster before I split the main file into smaller modules, taking ~5 minutes in serial, but I was unable to get any coverage information out of it. Not sure how/why that is the case, but thought it was worth mentioning as an interesting tidbit.


In my testing so far, bashunit::coverage::is_executable_line can be optimized from running up to 7 subshells of $(echo "$line" | "$GREP" [...] || true) for the various omissions, to at most 1 with the following (this includes the changes made in main for handling loop terminators with redirections/etc. which haven't made it to a released version yet):

_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY=(
# Skip function declaration lines but not single-line functions with a body'(function[[:space:]]+)?[a-zA-Z_][a-zA-Z0-9_:]*[[:space:]]*\(\)[[:space:]]*\{?'# Skip lines with only braces'[\{\}]'# Skip control flow keywords'(then|else|fi|do|done|esac|in|;;|;;&|;&)'# Skip loop terminator with trailing redirection/pipe/fd (e.g. "done < file", "done | sort", "done 2>&1", "done &")'done[[:space:]]*(<(<<)?|\||&|[[:digit:]]?>).*?'# Skip case patterns'[^\)]+\)'# Skip standalone ) for arrays/subshells'\)'
)
# Join the array using `|`, then reset IFS afterward# NOTE: Not sure if this is bash 3 compatible, but not difficult to convert to a loop of string concatenations if it isn't
temp_ifs="$IFS"
IFS='|'# Pre-compiled regex pattern of patterns (performance optimization)# Skip any of the above patterns in isolation as well as followed by an optional comment
_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS="^[[:space:]]*((${_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS_ARRAY[*]})[[:space:]]*)?(#.*)?$"
IFS="$temp_ifs"unset temp_ifs
functionbashunit::coverage::is_executable_line() {
local line="$1"local lineno="$2"# Unused but kept for API compatibility:"$lineno"# Skip empty lines (line with only whitespace)
[ -z"${line///}" ] &&return 1
# Skip lines matching any of the patterns above in _BASHUNIT_NONEXECUTABLE_LINE_PATTERS
[ "$(echo "$line"|"$GREP" -cE "$_BASHUNIT_NONEXECUTABLE_LINE_PATTERNS"|| true)"-gt 0 ] &&return 1
return 0
}

Making the above changes in my local copy of bashunit reduces the overall time to around 65 minutes on average (~3.8x faster coverage generation). In a smaller single file test (32 executable lines of code) it reduced overall time for lcov + HTML coverage output from a little over 4m7s down to a little under 1m21s.

While I haven't gone looking for much else past this yet, I get the feeling there are probably more cases like this where repeated subshell invocations can be grouped into single, more powerful subshell invocations with a little ingenuity.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions