The following sections relate to code in the native/c folder.
The C++ application includes a ZLogger singleton class for centralized logging. When ZLOG_ENABLE is defined during compilation, the logger automatically creates a .zowex/logs/ directory in the current user's home directory and writes log output to ~/.zowex/logs/zowex.log. Logs are written per-user (rather than to a location shared by all users of the binary, such as next to the executable) so that concurrent users of a shared zowex installation don't contend for the same log file. The zowex server daemon similarly writes its logs to ~/.zowex/logs/zowex_server.log.
ZLOG_ENABLE is defined when building in debug mode with make -DBuildType=DEBUG. You can exclusively enable logging by passing it directly to the make command: make -DZLOG_ENABLE=1
Once enabled, you can insert log statements into your source code by making calls to the logger class:
int main() {
ZLogger::get_instance().trace("This is a trace message: %s", "details here");
}Or, you can use the macros:
ZLOG_TRACE("This is also a trace message");Metal C has a similar set of macros in the format ZLOGXMSG:
ZLOGDMSG("Debug message");Where X is the desired log level for the message (outlined below in the "Log Levels" section).
The logger supports the following log levels (in order of verbosity):
TRACE(T) - Most verbose, includes all debugging informationDEBUG(D) - Debugging informationINFO(I) - General informational messages (default)WARN(W) - Warning messagesERROR(E) - Error messagesFATAL(F) - Fatal errors that may cause program terminationOFF- Disable all logging
Set the log level for zowex using the ZOWEX_LOG_LEVEL environment variable:
# Start zowex in interactive mode with log level "TRACE"
ZOWEX_LOG_LEVEL=TRACE ./zowex --itTo modify the log level programmatically in C++ code, use the ZLogger::set_log_level function. To change the log level in Metal C code, use the ZLGSTLVL function defined in zlogger_metal.h. Both functions take an integer from the enum zlog_level_t (also redefined as LogLevel in the C++ header file).
Override where log files are written using the ZOWEX_LOGS_DIR environment variable. This is useful for shared installations where users may not have write access to the default location, or for redirecting logs elsewhere for troubleshooting:
# Write logs to a custom directory instead of ~/.zowex/logs
ZOWEX_LOGS_DIR=/tmp/my-zowex-logs ./zowex --itThe zowex server daemon rolls its log file over once it exceeds 100KB, keeping up to 10 generations (zowex_server.log, zowex_server.log.1, ..., zowex_server.log.9) in FIFO order — the oldest generation is dropped to make room for the newest, bounding total disk usage per user to roughly 1MB.
Longer term, we may be able to make use of Catch2. However, with our testing of Metal C code, we are exploring custom-written infrastructure with the ability to handle abends or other z-specific scenarios. So, in the near term, we will use custom testing infrastructure found in native/c/test:
ztest.hppztest.cpp
This infrastructure requires ibm-clang compiler to enable "new" language features (lambdas) in order to provide a ~Jest testing syntax.
Tests are written in jest-style:
void my_tests()
{
describe("my tests",
[]() -> void
{
it("should pass the test",
[]() -> void
{
int rc = 0;
Expect(rc).ToBe(0);
});
});
}Tests also support before and after hooks, allowing you to clean up resources before/after tests are finished running.
describe("an_extern_function tests",
[]() -> void
{
int* x;
beforeAll([&x]() -> void {
x = (int*)malloc(sizeof(int));
memset(x, 0, sizeof(int));
});
beforeEach([&x]() -> void {
*x = -1;
});
afterAll([&x]() -> void {
free(x);
x = nullptr;
});
it("should increment x",
[&x]() -> void
{
an_extern_function(x, true);
Expect(*x).ToBe(0);
});
it("should not increment x",
[&x]() -> void
{
an_extern_function(x, false);
Expect(*x).ToBe(-1);
});
});Tests are built via a makefile in c/test and are also a default build target for the project root makefile.
Tests may be run from the c/test folder via make test or from client project using npm run z:test.
Test results are printed as they are executed, potentially interlaced with joblog output if using _BPXK_JOBLOG=STDERR, e.g.:
zstorage tests
PASS should obtain and free 31-bit storage
PASS should obtain and free 64-bit storage
zut tests
PASS should upper case and truncate a long string
PASS should upper case and pad a short string
zjb tests
PASS should be able to list a job
07.29.29 JOB01916 $HASP100 IEFBR14$ ON INTRDR FROM STC01874 DKELOSKY
07.29.29 JOB01916 IRR010I USERID DKELOSKY IS ASSIGNED TO THIS JOB.Summary results are printed upon completion, e.g.:
======== TESTS SUMMARY ========
Total Suites: 5 passed, 1 failed, 6 total
Tests: : 13 passed, 1 failed, 14 totalBy default, LE or Metal C abends will signal for program termination. When this occurs, a message may appear:
unexpected ABEND occurred. Add `TEST_OPTIONS.remove_signal_handling = true` to `it(...)` to capture abend dumpIn this situation, no CEEDUMP is captured. To disable this behavior, disable signal handling by passing a TEST_OPTIONS object as a parameter to it(), e.g.:
TEST_OPTIONS opts = {};
opts.remove_signal_handling = true;
it("should recover from an abend", []() -> void
{
int rc = ZRCVYEN();
Expect(rc).ToBe(0); }, opts);
});Tests have a default timeout of 10 seconds to prevent hung tests. For tests that need more time (e.g., mainframe I/O operations), the timeout can be increased:
TEST_OPTIONS opts = {};
opts.timeout_sec = 30;
it("should complete slow operation", []() -> void
{
// Test with 30 second timeout
}, opts);If timeout_sec is 0, the default timeout of 10 seconds applies to tests and hooks.
The testing infrastructure provides the following APIs
describe(name, fn) creates a block that groups related tests. One top level describe() must exist and nesting is not yet supported.
it(name, fn) is a test case contained within a describe() block. Individual tests and assertions are achieved with expect() calls and are contained within the fn provided by the second operand.
The expect() function is used to test a value (int, string, pointer, etc...) which returns a RESULT_CHECK object. To assert that a result matches an expected value,
use the ToBe() function on the RESULT_CHECK object.
Multiple expect() functions can exist within a test() to test multiple scenarios. However, if an expect() fails, the remaining expect() calls in a test() are skipped.
Expect(value) and ExpectWithContext(value, context) are two C macros (#defines) which provide extra information whenever an expect fails. These provide the source filename and line number for which expect failed. ExpectWithContext() allows providing a second parameter to provide extra debugging information alongside a failed test.
For example:
ExpectWithContext(5, "Failed to submit job").ToBe(3);Produces:
FAIL should fail
expected int '5' to be '3'
at ./zjb.test.cpp:132 (Failed to submit job)Use .ToBe() to compare values set in RESULT_CHECK through expect. If the expected value and checked value do not match, an exception is thrown and the test fails.
Use .Not() to inverse compare values behavior. Not() returns a RESULT_CHECK object which is then checked vai ToBE().
To create a C header from an HLASM DSECT:
- Create the
asmchdrelement, e.g.cvt.s - Generate & download the header via
npm run z:chdsect cvt.s - Save the file to ensure proper formating
OR:
- create a
.sfile inasmchdrfolder, e.g.asasymbp.s - upload, e.g.
npm run z:upload asmchdr/asasymbp.s - allocate output adata data set if none exists, e.g.
zowex data-set create-adata <hlq>.adata - allocate output chdr data set if none exists, e.g.
zowex data-set create-vb <hlq>.chdr - build
.sfile, e.g.as -madata --gadata="//'<hlq>.USER(ASASYMBP)'" asasymbp.s - convert the file via
ccnedsct, e.g.zowex tool ccnedsct --ad "<hlq>.adata(asasymbp)" --cd "<hlq>.chdr(asasymbp)" - copy, download the
.hfile where needed, e.g.zowe files download ds "<hlq>.chdr(asasymbp)" --file native/c/chdsect/asasymbp.h
In order to use ESTAEX-type recovery, Language Environment (LE) must be enabled with TRAP(ON,NOSPIE). Otherwise, ESPIE recovery will gain control in an
abend scenario and bypass any established ESTAEX.
_CEE_RUNOPTShttps://www.ibm.com/docs/en/zos/3.1.0?topic=options-how-specify-runtimeTRAP(ON,NOSPIE)https://www.ibm.com/docs/en/zos/3.1.0?topic=ulero-trap
You can add temporary debugging messages within Metal C code which will issue WTO messages with a message prefix of ZWEX0001I. These messages may be used in development only
and must not appear in distributed versions of zowex.
To see these debugging messages within z/OS UNIX, set the environment variable export _BPXK_JOBLOG=STDERR. Then add debug messages via zwto_debug(...) found within zwto.h in the same format as C printf format strings, e.g. zwto_debug("return code was %d", rc);.
Raw storage address contents can be printed to the console or a file to help with debugging scenarios. This can be achieved using the zdbg.h header file.
To dump storage in LE, use:
#include "zdbg.h"
int data = 3;
zut_dump_storage(title, &data, sizeof(data));You must ensure zut_alloc_debug() is called to allocate an output DD for log messages. Then in Metal C, use
#include "zdbg.h"
ZJB zjb = {0};
zut_dump_storage("ZJB", zjb, sizeof(ZJB), ZUTDBGMG);By default, output is printed to /tmp/zowex_debug_<PID>.txt (where PID is the current zowex process ID) when using ZUTDBGMG(); however, you may provide a Metal C compatible alternative.