Repository: numba/numba

Issue:
Add figure for total memory to `numba -s` output.
## Feature request

It'd be useful to have the amount of memory available on a machine added to the `numba -s` output. This is useful to know for cases where e.g. swapping might be occurring.

This is a good first issue, work involves:

1. Find a way to get the total available memory using Python standard libraries or NumPy.
2. Print the information in 1. in this section: https://github.com/numba/numba/blob/master/numba/misc/numba_entry.py#L34 such that `numba -s` reports the amount of available memory.





Discussion:
It seems that [psutil](https://github.com/giampaolo/psutil) would be the way to go. It claims it supports a range of platforms (Unix-like, Windows, etc.). Can a dependency be added or is it preferable to write independent code? I'd be quite happy to work on this.
If writing independent code becomes an inferior re-implementation of psutil, it might be better to have an optional dependency on psutil (with memory information omitted if psutil is not present).
That would be my preference indeed, maintaining the code for several platforms may become tedious and there could be difficulties with testing.
depending on the size and license of `psutil` it may make sense to vendor the library so as to avoid having to spec, resolve and install another dependency .
The psutil folder on my machine is 1.6 MB and the software goes under BSD 3-Clause "New" or "Revised" License.
Given vendoring `psutil` involves compiling in largish C extensions etc., I think we can either cover linux, osx, windows, which should be reasonably ok to do given it's just reading a value with a variety of methods available to do this. Or add an optional dep on `psutil`, the field in `numba -s` can just have "install psutil to see this" if `psutil` isn't present.
I've done a bit of research for linux, osx and windows. In summary:

- **Linux**: [`cat /proc/meminfo`](http://man7.org/linux/man-pages/man5/proc.5.html) gives total, free and available memory as well as swap total and swap free
- **Mac OS**: this seems a bit more tricky. There is a relevant [discussion](https://github.com/giampaolo/psutil/issues/1277) about the way it is calculated in `psutil`. `psutil`'s function for Mac is written in C and makes use of  [`sysctl`](https://opensource.apple.com/source/xnu/xnu-1456.1.26/bsd/sys/sysctl.h.auto.html) library. 
- **Windows**: `psutil`'s function is written in C and uses [`Psapi`](https://docs.microsoft.com/en-gb/windows/win32/api/psapi/ns-psapi-performance_information) library.

Writing a short module in C is within reach for me (although I did that some time ago) and the code in `psutil` gives a good indication of what to look for.

I don't have a Mac available for testing, but I have Linux and a Windows 10 virtual machine.      
Thanks for looking a this, whilst a C module would likely work, it has a maintenance cost that needs to be balanced against the use. I'm wondering what your thoughts are on a more basic alternative, perhaps:

* Linux: read `/proc/meminfo` as we do for e.g. CFS groups:
  https://github.com/numba/numba/blob/ce2139c7dd93127efb04b35f28f4bebc7f44dfd5/numba/misc/numba_entry.py#L61-L64
* OSX: open a subprocess, call `sysctl hw.memsize`, capture output
* Windows: Perhaps `ctypes.windll.kernel32` call to https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-globalmemorystatusex ?
@stuartarchibald My first impression was that running shell commands via a subprocess or reading files would introduce some parsing code which can sometimes look a bit verbose. However, this option would be written in Python and be easy to debug.

I have summarized the pros and cons of the different options we have so far (please feel free to edit):

- **psutil**
    - Pros: clean code, results returned as a named tuple, can add features later
    - Cons: need optional dependency

- **Subprocesses / file parsing**
    - Pros: internal code, no dependency, written in Python, easy to debug
    - Cons: parsing code can be verbose and break later, adding features requires new parsing code

- **C module**
    - Pros: internal code, features can be added later, results can be used programmatically
    - Cons: C code maintenance, platform-dependent build, debugging may be less easy
My vote is for optional dependency on psutil.  It is relatively easy to tell someone to conda/pip install it if they don't have it and we need additional information.
My first vote is for a **Subprocessing / file parsing** solution provided that it doesn't turn into a deep rabbit hole. I would like to see the amount of memory that the machine has by default when executing `numba -s` and I don't believe that this should be optional. If we can get that working with a small, self contained and robust solution that would be my favored approach. If that is not feasible and I am underestimating complexity here then an optional dependency on `psutil` would also be acceptable for me too, however.
I found a Windows command that can be used to get the total and available memory:

    systeminfo | findstr /C:"Memory"

That makes me lean towards the subprocessing option. I need someone with a mac to give me the output of `sysctl hw.memsize` and `vmstat` as we may need both to get the available memory.
I think I'd also prefer some version of it to just work OOTB. It seems like it'd be pretty cheap (probably <30 lines) to find something that works most-of-the-time even if not perfect. I think going with just total memory is fine as a start. None of this precludes looking for `psutil` in the env to start with as a shortcut.

Requested example output on a mac:
```
$ sysctl hw.memsize
hw.memsize: 17179869184
```
Considering that in the future we will return a dictionary of the system info, I've written a [gist](https://gist.github.com/jagaudin/30ba6eda936b48aeae3646918b4ba602) to show how I would proceed. Let me know if you have any comments, otherwise will proceed on this basis.




Resolving patch:
diff --git a/numba/misc/numba_entry.py b/numba/misc/numba_entry.py
--- a/numba/misc/numba_entry.py
+++ b/numba/misc/numba_entry.py
@@ -2,362 +2,9 @@
 import argparse
 import os
 import subprocess
+import json
 
-
-def get_sys_info():
-    # delay these imports until now as they are only needed in this
-    # function which then exits.
-    import platform
-    import json
-    import multiprocessing
-    from numba.core import config
-    from numba import cuda as cu
-    from numba.cuda import cudadrv
-    from numba.cuda.cudadrv.driver import driver as cudriver
-    from numba import roc
-    from numba.roc.hlc import hlc, libhlc
-    import textwrap as tw
-    import ctypes as ct
-    import llvmlite.binding as llvmbind
-    import locale
-    from datetime import datetime
-    from itertools import chain
-    from subprocess import check_output, CalledProcessError
-
-    try:
-        fmt = "%-45s : %-s"
-        print("-" * 80)
-        print("__Time Stamp__")
-        print(datetime.utcnow())
-        print("")
-
-        print("__Hardware Information__")
-        system_name = platform.system()
-        print(fmt % ("Machine", platform.machine()))
-        print(fmt % ("CPU Name", llvmbind.get_host_cpu_name()))
-        if system_name == 'Linux':
-            strmatch = 'Cpus_allowed'
-            try:
-                loc = '/proc/self/status'
-                with open(loc, 'rt') as f:
-                    proc_stat = f.read().splitlines()
-                    for x in proc_stat:
-                        if x.startswith(strmatch):
-                            if x.startswith('%s:' % strmatch):
-                                hexnum = '0x%s' % x.split(':')[1].strip()
-                                acc_cpus = int(hexnum, 16)
-                                _n = str(bin(acc_cpus).count('1'))
-                                print(fmt % ("Number of accessible CPU cores",
-                                                _n))
-                            elif x.startswith('%s_list:' % strmatch):
-                                _a = x.split(':')[1].strip()
-                                print(fmt % ("Listed accessible CPUs cores",
-                                                _a))
-            except Exception:
-                print(fmt % ("CPU count", multiprocessing.cpu_count()))
-            # See if CFS is in place
-            # https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt
-            try:
-                def scrape_lines(loc):
-                    with open(loc, 'rt') as f:
-                        return f.read().splitlines()
-                loc = '/sys/fs/cgroup/cpuacct/cpu.cfs_period_us'
-                cfs_period = int(scrape_lines(loc)[0])
-                loc = '/sys/fs/cgroup/cpuacct/cpu.cfs_quota_us'
-                cfs_quota = int(scrape_lines(loc)[0])
-                if cfs_quota == -1:
-                    print(fmt % ("CFS restrictions", "None"))
-                else:
-                    runtime_amount = float(cfs_quota)/float(cfs_period)
-                    print(fmt % ("CFS restrictions (CPUs worth of runtime)",
-                                 runtime_amount))
-            except Exception:
-                print(fmt % ("CFS restrictions", 'Information not available'))
-        else:
-            print(fmt % ("CPU count", multiprocessing.cpu_count()))
-
-        try:
-            featuremap = llvmbind.get_host_cpu_features()
-        except RuntimeError:
-            print(fmt % ("CPU Features", "NA"))
-        else:
-            features = sorted([key for key, value in featuremap.items()
-                               if value])
-            cpu_feat = tw.fill(' '.join(features), 80)
-            print(fmt % ("CPU Features", ""))
-            print(cpu_feat)
-        print("")
-
-        print("__OS Information__")
-        print(fmt % ("Platform", platform.platform(aliased=True)))
-        print(fmt % ("Release", platform.release()))
-        print(fmt % ("System Name", system_name))
-        print(fmt % ("Version", platform.version()))
-        try:
-            if system_name == 'Linux':
-                info = ()
-            elif system_name == 'Windows':
-                info = platform.win32_ver()
-            elif system_name == 'Darwin':
-                info = platform.mac_ver()
-            else:
-                raise RuntimeError("Unknown system.")
-            buf = ''.join([x
-                           if x != '' else ' '
-                           for x in list(chain.from_iterable(info))])
-            print(fmt % ("OS specific info", buf))
-
-            if system_name == 'Linux':
-                print(fmt % ("glibc info", ' '.join(platform.libc_ver())))
-        except:
-            print("Error: System name incorrectly identified or unknown.")
-        print("")
-
-        print("__Python Information__")
-        print(fmt % ("Python Compiler", platform.python_compiler()))
-        print(
-            fmt %
-            ("Python Implementation",
-             platform.python_implementation()))
-        print(fmt % ("Python Version", platform.python_version()))
-        lcl = []
-        try:
-            for x in locale.getdefaultlocale():
-                if x is not None:
-                    lcl.append(x)
-        except Exception as e:
-            lcl.append(str(e))
-        print(fmt % ("Python Locale ", ' '.join(lcl)))
-
-        print("")
-        print("__LLVM information__")
-        print(
-            fmt %
-            ("LLVM version", '.'.join(
-                [str(k) for k in llvmbind.llvm_version_info])))
-
-        print("")
-        print("__CUDA Information__")
-        # Look for GPUs
-        try:
-            cu.list_devices()[0]  # will a device initialise?
-        except Exception as e:
-            msg_not_found = "CUDA driver library cannot be found"
-            msg_disabled_by_user = "CUDA is disabled"
-            msg_end = " or no CUDA enabled devices are present."
-            msg_generic_problem = "Error: CUDA device intialisation problem."
-            msg = getattr(e, 'msg', None)
-            if msg is not None:
-                if msg_not_found in msg:
-                    err_msg = msg_not_found + msg_end
-                elif msg_disabled_by_user in msg:
-                    err_msg = msg_disabled_by_user + msg_end
-                else:
-                    err_msg = msg_generic_problem + " Message:" + msg
-            else:
-                err_msg = msg_generic_problem + " " + str(e)
-            # Best effort error report
-            print("%s\nError class: %s" % (err_msg, str(type(e))))
-        else:
-            try:
-                cu.detect()
-                dv = ct.c_int(0)
-                cudriver.cuDriverGetVersion(ct.byref(dv))
-                print(fmt % ("CUDA driver version", dv.value))
-                print("CUDA libraries:")
-                cudadrv.libs.test(sys.platform, print_paths=False)
-            except:
-                print(
-                    "Error: Probing CUDA failed (device and driver present, runtime problem?)\n")
-
-        print("")
-        print("__ROC Information__")
-        roc_is_available = roc.is_available()
-        print(fmt % ("ROC available", roc_is_available))
-
-        toolchains = []
-        try:
-            libhlc.HLC()
-            toolchains.append('librocmlite library')
-        except:
-            pass
-        try:
-            cmd = hlc.CmdLine().check_tooling()
-            toolchains.append('ROC command line tools')
-        except:
-            pass
-
-        # if no ROC try and report why
-        if not roc_is_available:
-            from numba.roc.hsadrv.driver import hsa
-            try:
-                hsa.is_available
-            except Exception as e:
-                msg = str(e)
-            else:
-               msg = 'No ROC toolchains found.'
-            print(fmt % ("Error initialising ROC due to", msg))
-
-        if toolchains:
-            print(fmt % ("Available Toolchains", ', '.join(toolchains)))
-
-        try:
-            # ROC might not be available due to lack of tool chain, but HSA
-            # agents may be listed
-            from numba.roc.hsadrv.driver import hsa, dgpu_count
-            decode = lambda x: x.decode('utf-8') if isinstance(x, bytes) else x
-            print("\nFound %s HSA Agents:" % len(hsa.agents))
-            for i, agent in enumerate(hsa.agents):
-                print('Agent id  : %s' % i)
-                print('    vendor: %s' % decode(agent.vendor_name))
-                print('      name: %s' % decode(agent.name))
-                print('      type: %s' % agent.device)
-                print("")
-
-            _dgpus = []
-            for a in hsa.agents:
-                if a.is_component and a.device == 'GPU':
-                   _dgpus.append(decode(a.name))
-            print(fmt % ("Found %s discrete GPU(s)" % dgpu_count(), \
-                  ', '.join(_dgpus)))
-        except Exception as e:
-            print("No HSA Agents found, encountered exception when searching:")
-            print(e)
-
-
-        print("")
-        print("__SVML Information__")
-        # replicate some SVML detection logic from numba.__init__ here.
-        # if SVML load fails in numba.__init__ the splitting of the logic
-        # here will help diagnosis of the underlying issue
-        have_svml_library = True
-        try:
-            if sys.platform.startswith('linux'):
-                llvmbind.load_library_permanently("libsvml.so")
-            elif sys.platform.startswith('darwin'):
-                llvmbind.load_library_permanently("libsvml.dylib")
-            elif sys.platform.startswith('win'):
-                llvmbind.load_library_permanently("svml_dispmd")
-            else:
-                have_svml_library = False
-        except:
-            have_svml_library = False
-        func = getattr(llvmbind.targets, "has_svml", None)
-        llvm_svml_patched = func() if func is not None else False
-        svml_operational = (config.USING_SVML and llvm_svml_patched \
-                            and have_svml_library)
-        print(fmt % ("SVML state, config.USING_SVML", config.USING_SVML))
-        print(fmt % ("SVML library found and loaded", have_svml_library))
-        print(fmt % ("llvmlite using SVML patched LLVM", llvm_svml_patched))
-        print(fmt % ("SVML operational", svml_operational))
-
-        # Check which threading backends are available.
-        print("")
-        print("__Threading Layer Information__")
-        def parse_error(e, backend):
-            # parses a linux based error message, this is to provide feedback
-            # and hide user paths etc
-            try:
-                path, problem, symbol =  [x.strip() for x in e.msg.split(':')]
-                extn_dso = os.path.split(path)[1]
-                if backend in extn_dso:
-                    return "%s: %s" % (problem, symbol)
-            except Exception:
-                pass
-            return "Unknown import problem."
-
-        try:
-            from numba.np.ufunc import tbbpool
-            print(fmt % ("TBB Threading layer available", True))
-        except ImportError as e:
-            # might be a missing symbol due to e.g. tbb libraries missing
-            print(fmt % ("TBB Threading layer available", False))
-            print(fmt % ("+--> Disabled due to",
-                         parse_error(e, 'tbbpool')))
-
-        try:
-            from numba.np.ufunc import omppool
-            print(fmt % ("OpenMP Threading layer available", True))
-            print(fmt % ("+--> Vendor: ", omppool.openmp_vendor))
-        except ImportError as e:
-            print(fmt % ("OpenMP Threading layer available", False))
-            print(fmt % ("+--> Disabled due to",
-                         parse_error(e, 'omppool')))
-
-        try:
-            from numba.np.ufunc import workqueue
-            print(fmt % ("Workqueue Threading layer available", True))
-        except ImportError as e:
-            print(fmt % ("Workqueue Threading layer available", False))
-            print(fmt % ("+--> Disabled due to",
-                         parse_error(e, 'workqueue')))
-
-        # look for numba env vars that are set
-        print("")
-        print("__Numba Environment Variable Information__")
-        _envvar_found = False
-        for k, v in os.environ.items():
-            if k.startswith('NUMBA_'):
-                print(fmt % (k, v))
-                _envvar_found = True
-        if not _envvar_found:
-            print("None set.")
-
-        # Look for conda and conda information
-        print("")
-        print("__Conda Information__")
-        cmd = ["conda", "info", "--json"]
-        try:
-            conda_out = check_output(cmd)
-        except Exception as e:
-            print(
-                "Conda not present/not working.\nError was %s\n" % e)
-        else:
-            data = ''.join(conda_out.decode("utf-8").splitlines())
-            jsond = json.loads(data)
-            keys = ['conda_build_version',
-                    'conda_env_version',
-                    'platform',
-                    'python_version',
-                    'root_writable']
-            for k in keys:
-                try:
-                    print(fmt % (k, jsond[k]))
-                except KeyError:
-                    pass
-
-            # get info about current environment
-            cmd = ["conda", "list"]
-            try:
-                conda_out = check_output(cmd)
-            except CalledProcessError as e:
-                print("Error: Conda command failed. Error was %s\n" % e.output)
-            else:
-                print("")
-                print("__Current Conda Env__")
-                data = conda_out.decode("utf-8").splitlines()
-                for k in data:
-                    if k[0] != '#':  # don't show where the env is, personal data
-                        print(k)
-
-        print("-" * 80)
-
-    except Exception as e:
-        print("Error: The system reporting tool has failed unexpectedly.")
-        print("Exception was:")
-        print(e)
-
-    finally:
-        print(
-            "%s" %
-            "If requested, please copy and paste the information between\n"
-            "the dashed (----) lines, or from a given specific section as\n"
-            "appropriate.\n\n"
-            "=============================================================\n"
-            "IMPORTANT: Please ensure that you are happy with sharing the\n"
-            "contents of the information present, any information that you\n"
-            "wish to keep private you should remove before sharing.\n"
-            "=============================================================\n")
+from .numba_sysinfo import display_sysinfo, get_sysinfo
 
 
 def make_parser():
@@ -378,6 +25,8 @@ def make_parser():
                         help='Output source annotation as html')
     parser.add_argument('-s', '--sysinfo', action="store_true",
                         help='Output system information for bug reporting')
+    parser.add_argument('--sys-json', nargs=1,
+                        help='Saves the system info dict as a json file')
     parser.add_argument('filename', nargs='?', help='Python source filename')
     return parser
 
@@ -395,7 +44,15 @@ def main():
 
     if args.sysinfo:
         print("System info:")
-        get_sys_info()
+        display_sysinfo()
+        sys.exit(0)
+
+    if args.sys_json:
+        info = get_sysinfo()
+        info.update({'Start': info['Start'].isoformat()})
+        info.update({'Start UTC': info['Start UTC'].isoformat()})
+        with open(args.sys_json[0], 'w') as f:
+            json.dump(info, f, indent=4)
         sys.exit(0)
 
     os.environ['NUMBA_DUMP_ANNOTATION'] = str(int(args.annotate))
diff --git a/numba/misc/numba_sysinfo.py b/numba/misc/numba_sysinfo.py
new file mode 100644
--- /dev/null
+++ b/numba/misc/numba_sysinfo.py
@@ -0,0 +1,659 @@
+import ctypes
+import json
+import locale
+import multiprocessing
+import os
+import platform
+import textwrap
+import sys
+from contextlib import redirect_stdout
+from datetime import datetime
+from io import StringIO
+from subprocess import check_output, PIPE, CalledProcessError
+import llvmlite.binding as llvmbind
+from numba import cuda as cu
+from numba import roc
+from numba.roc.hlc import hlc, libhlc
+from numba.cuda import cudadrv
+from numba.cuda.cudadrv.driver import driver as cudriver
+from numba.core import config
+
+_psutil_import = False
+try:
+    import psutil
+except ImportError:
+    pass
+else:
+    _psutil_import = True
+
+__all__ = ['get_sysinfo', 'display_sysinfo']
+
+# Keys of a `sysinfo` dictionary
+
+# Time info
+_start, _start_utc, _runtime = 'Start', 'Start UTC', 'Runtime'
+# Hardware info
+_machine = 'Machine'
+_cpu_name, _cpu_count = 'CPU Name', 'CPU Count'
+_cpus_allowed, _cpus_list = 'CPUs Allowed', 'List CPUs Allowed'
+_cpu_features = 'CPU Features'
+_cfs_quota, _cfs_period = 'CFS Quota', 'CFS Period',
+_cfs_restrict = 'CFS Restriction'
+_mem_total, _mem_available = 'Mem Total', 'Mem Available'
+# OS info
+_platform_name, _platform_release = 'Platform Name', 'Platform Release'
+_os_name, _os_version = 'OS Name', 'OS Version'
+_os_spec_version = 'OS Specific Version'
+_libc_version = 'Libc Version'
+# Python info
+_python_comp = 'Python Compiler'
+_python_impl = 'Python Implementation'
+_python_version = 'Python Version'
+_python_locale = 'Python Locale'
+# LLVM info
+_llvm_version = 'LLVM Version'
+# CUDA info
+_cu_dev_init = 'CUDA Device Init'
+_cu_drv_ver = 'CUDA Driver Version'
+_cu_detect_out, _cu_lib_test = 'CUDA Detect Output', 'CUDA Lib Test'
+# ROC information
+_roc_available, _roc_toolchains = 'ROC Available', 'ROC Toolchains'
+_hsa_agents_count, _hsa_agents = 'HSA Agents Count', 'HSA Agents'
+_hsa_gpus_count, _hsa_gpus = 'HSA Discrete GPUs Count', 'HSA Discrete GPUs'
+# SVML info
+_svml_state, _svml_loaded = 'SVML State', 'SVML Lib Loaded'
+_llvm_svml_patched = 'LLVM SVML Patched'
+_svml_operational = 'SVML Operational'
+# Threading layer info
+_tbb_thread, _tbb_error = 'TBB Threading', 'TBB Threading Error'
+_openmp_thread, _openmp_error = 'OpenMP Threading', 'OpenMP Threading Error'
+_openmp_vendor = 'OpenMP vendor'
+_wkq_thread, _wkq_error = 'Workqueue Threading', 'Workqueue Threading Error'
+# Numba info
+_numba_env_vars = 'Numba Env Vars'
+# Conda info
+_conda_build_ver, _conda_env_ver = 'Conda Build', 'Conda Env'
+_conda_platform, _conda_python_ver = 'Conda Platform', 'Conda Python Version'
+_conda_root_writable = 'Conda Root Writable'
+# Packages info
+_inst_pkg = 'Installed Packages'
+# Psutil info
+_psutil = 'Psutil Available'
+# Errors and warnings
+_errors = 'Errors'
+_warnings = 'Warnings'
+
+# Error and warning log
+_error_log = []
+_warning_log = []
+
+
+def get_os_spec_info(os_name):
+    # Linux man page for `/proc`:
+    # http://man7.org/linux/man-pages/man5/proc.5.html
+
+    # Windows documentation for `wmic OS`:
+    # https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-operatingsystem
+
+    # MacOS man page for `sysctl`:
+    # https://www.unix.com/man-page/osx/3/sysctl/
+    # MacOS man page for `vm_stat`:
+    # https://www.unix.com/man-page/osx/1/vm_stat/
+
+    class CmdBufferOut(tuple):
+        buffer_output_flag = True
+
+    class CmdReadFile(tuple):
+        read_file_flag = True
+
+    shell_params = {
+        'Linux': {
+            'cmd': (
+                CmdReadFile(('/sys/fs/cgroup/cpuacct/cpu.cfs_quota_us',)),
+                CmdReadFile(('/sys/fs/cgroup/cpuacct/cpu.cfs_period_us',)),
+            ),
+            'cmd_optional': (
+                CmdReadFile(('/proc/meminfo',)),
+                CmdReadFile(('/proc/self/status',)),
+            ),
+            'kwds': {
+                # output string fragment -> result dict key
+                'MemTotal:': _mem_total,
+                'MemAvailable:': _mem_available,
+                'Cpus_allowed:': _cpus_allowed,
+                'Cpus_allowed_list:': _cpus_list,
+                '/sys/fs/cgroup/cpuacct/cpu.cfs_quota_us': _cfs_quota,
+                '/sys/fs/cgroup/cpuacct/cpu.cfs_period_us': _cfs_period,
+            },
+        },
+        'Windows': {
+            'cmd': (),
+            'cmd_optional': (
+                CmdBufferOut(('wmic', 'OS', 'get', 'TotalVirtualMemorySize')),
+                CmdBufferOut(('wmic', 'OS', 'get', 'FreeVirtualMemory')),
+            ),
+            'kwds': {
+                # output string fragment -> result dict key
+                'TotalVirtualMemorySize': _mem_total,
+                'FreeVirtualMemory': _mem_available,
+            },
+        },
+        'Darwin': {
+            'cmd': (),
+            'cmd_optional': (
+                ('sysctl', 'hw.memsize'),
+                ('vm_stat'),
+            ),
+            'kwds': {
+                # output string fragment -> result dict key
+                'hw.memsize:': _mem_total,
+                'free:': _mem_available,
+            },
+            'units': {
+                _mem_total: 1,  # Size is given in bytes.
+                _mem_available: 4096,  # Size is given in 4kB pages.
+            },
+        },
+    }
+
+    os_spec_info = {}
+    params = shell_params.get(os_name, {})
+    cmd_selected = params.get('cmd', ())
+
+    if _psutil_import:
+        vm = psutil.virtual_memory()
+        os_spec_info.update({
+            _mem_total: vm.total,
+            _mem_available: vm.available,
+        })
+        p = psutil.Process()
+        cpus_allowed = p.cpu_affinity() if hasattr(p, 'cpu_affinity') else []
+        if cpus_allowed:
+            os_spec_info[_cpus_allowed] = len(cpus_allowed)
+            os_spec_info[_cpus_list] = ' '.join(str(n) for n in cpus_allowed)
+
+    else:
+        _warning_log.append(
+            "Warning (psutil): psutil cannot be imported. "
+            "For more accuracy, consider installing it.")
+        # Fallback to internal heuristics
+        cmd_selected += params.get('cmd_optional', ())
+
+    # Assuming the shell cmd returns a unique (k, v) pair per line
+    # or a unique (k, v) pair spread over several lines:
+    # Gather output in a list of strings containing a keyword and some value.
+    output = []
+    for cmd in cmd_selected:
+        if hasattr(cmd, 'read_file_flag'):
+            # Open file within Python
+            with open(cmd[0], 'r') as f:
+                out = f.readlines()
+                if out:
+                    out[0] = ' '.join((cmd[0], out[0]))
+                    output.extend(out)
+            continue
+        else:
+            # Spawn a subprocess
+            try:
+                out = check_output(cmd, stderr=PIPE)
+            except (OSError, CalledProcessError) as e:
+                _error_log.append(f'Error (subprocess): {e}')
+                continue
+            if hasattr(cmd, 'buffer_output_flag'):
+                out = b' '.join(line for line in out.splitlines()) + b'\n'
+        output.extend(out.decode().splitlines())
+
+    # Extract (k, output) pairs by searching for keywords in output
+    kwds = params.get('kwds', {})
+    for line in output:
+        match = kwds.keys() & line.split()
+        if match and len(match) == 1:
+            k = kwds[match.pop()]
+            os_spec_info[k] = line
+        elif len(match) > 1:
+            print(f'Ambiguous output: {line}')
+
+    # Try to extract something meaningful from output string
+    def format():
+        # CFS restrictions
+        split = os_spec_info.get(_cfs_quota, '').split()
+        if split:
+            os_spec_info[_cfs_quota] = float(split[-1])
+        split = os_spec_info.get(_cfs_period, '').split()
+        if split:
+            os_spec_info[_cfs_period] = float(split[-1])
+        if os_spec_info.get(_cfs_quota, -1) != -1:
+            cfs_quota = os_spec_info.get(_cfs_quota, '')
+            cfs_period = os_spec_info.get(_cfs_period, '')
+            runtime_amount = cfs_quota / cfs_period
+            os_spec_info[_cfs_restrict] = runtime_amount
+
+    def format_optional():
+        # Memory
+        units = {_mem_total: 1024, _mem_available: 1024}
+        units.update(params.get('units', {}))
+        for k in (_mem_total, _mem_available):
+            digits = ''.join(d for d in os_spec_info.get(k, '') if d.isdigit())
+            os_spec_info[k] = int(digits or 0) * units[k]
+        # Accessible CPUs
+        split = os_spec_info.get(_cpus_allowed, '').split()
+        if split:
+            n = split[-1]
+            n = n.split(',')[-1]
+            os_spec_info[_cpus_allowed] = str(bin(int(n or 0, 16))).count('1')
+        split = os_spec_info.get(_cpus_list, '').split()
+        if split:
+            os_spec_info[_cpus_list] = split[-1]
+
+    try:
+        format()
+        if not _psutil_import:
+            format_optional()
+    except Exception as e:
+        _error_log.append(f'Error (format shell output): {e}')
+
+    # Call OS specific functions
+    os_specific_funcs = {
+        'Linux': {
+            _libc_version: lambda: ' '.join(platform.libc_ver())
+        },
+        'Windows': {
+            _os_spec_version: lambda: ' '.join(
+                s for s in platform.win32_ver()),
+        },
+        'Darwin': {
+            _os_spec_version: lambda: ''.join(
+                i or ' ' for s in tuple(platform.mac_ver()) for i in s),
+        },
+    }
+    key_func = os_specific_funcs.get(os_name, {})
+    os_spec_info.update({k: f() for k, f in key_func.items()})
+    return os_spec_info
+
+
+def get_sysinfo():
+
+    # Gather the information that shouldn't raise exceptions
+    sys_info = {
+        _start: datetime.now(),
+        _start_utc: datetime.utcnow(),
+        _machine: platform.machine(),
+        _cpu_name: llvmbind.get_host_cpu_name(),
+        _cpu_count: multiprocessing.cpu_count(),
+        _platform_name: platform.platform(aliased=True),
+        _platform_release: platform.release(),
+        _os_name: platform.system(),
+        _os_version: platform.version(),
+        _python_comp: platform.python_compiler(),
+        _python_impl: platform.python_implementation(),
+        _python_version: platform.python_version(),
+        _numba_env_vars: {k: v for (k, v) in os.environ.items()
+                          if k.startswith('NUMBA_')},
+        _llvm_version: '.'.join(str(i) for i in llvmbind.llvm_version_info),
+        _roc_available: roc.is_available(),
+        _psutil: _psutil_import,
+    }
+
+    # CPU features
+    try:
+        feature_map = llvmbind.get_host_cpu_features()
+    except RuntimeError as e:
+        _error_log.append(f'Error (CPU features): {e}')
+    else:
+        features = sorted([key for key, value in feature_map.items() if value])
+        sys_info[_cpu_features] = ' '.join(features)
+
+    # Python locale
+    # On MacOSX, getdefaultlocale can raise. Check again if Py > 3.7.5
+    try:
+        sys_info[_python_locale] = '.'.join(locale.getdefaultlocale())
+    except Exception as e:
+        _error_log.append(f'Error (locale): {e}')
+
+    # CUDA information
+    try:
+        cu.list_devices()[0]  # will a device initialise?
+    except Exception as e:
+        sys_info[_cu_dev_init] = False
+        msg_not_found = "CUDA driver library cannot be found"
+        msg_disabled_by_user = "CUDA is disabled"
+        msg_end = " or no CUDA enabled devices are present."
+        msg_generic_problem = "CUDA device intialisation problem."
+        msg = getattr(e, 'msg', None)
+        if msg is not None:
+            if msg_not_found in msg:
+                err_msg = msg_not_found + msg_end
+            elif msg_disabled_by_user in msg:
+                err_msg = msg_disabled_by_user + msg_end
+            else:
+                err_msg = msg_generic_problem + " Message:" + msg
+        else:
+            err_msg = msg_generic_problem + " " + str(e)
+        # Best effort error report
+        _warning_log.append("Warning (cuda): %s\nException class: %s" %
+                            (err_msg, str(type(e))))
+    else:
+        try:
+            sys_info[_cu_dev_init] = True
+
+            output = StringIO()
+            with redirect_stdout(output):
+                cu.detect()
+            sys_info[_cu_detect_out] = output.getvalue()
+            output.close()
+
+            dv = ctypes.c_int(0)
+            cudriver.cuDriverGetVersion(ctypes.byref(dv))
+            sys_info[_cu_drv_ver] = dv.value
+
+            output = StringIO()
+            with redirect_stdout(output):
+                cudadrv.libs.test(sys.platform, print_paths=False)
+            sys_info[_cu_lib_test] = output.getvalue()
+            output.close()
+        except Exception as e:
+            _warning_log.append(
+                "Warning (cuda): Probing CUDA failed "
+                "(device and driver present, runtime problem?)\n"
+                f"(cuda) {type(e)}: {e}")
+
+    # ROC information
+    # If no ROC try and report why
+    if not sys_info[_roc_available]:
+        from numba.roc.hsadrv.driver import hsa
+        try:
+            hsa.is_available
+        except Exception as e:
+            msg = str(e)
+        else:
+            msg = 'No ROC toolchains found.'
+        _warning_log.append(f"Warning (roc): Error initialising ROC: {msg}")
+
+    toolchains = []
+    try:
+        libhlc.HLC()
+        toolchains.append('librocmlite library')
+    except Exception:
+        pass
+    try:
+        cmd = hlc.CmdLine().check_tooling()
+        toolchains.append('ROC command line tools')
+    except Exception:
+        pass
+    sys_info[_roc_toolchains] = toolchains
+
+    try:
+        # ROC might not be available due to lack of tool chain, but HSA
+        # agents may be listed
+        from numba.roc.hsadrv.driver import hsa, dgpu_count
+
+        def decode(x):
+            return x.decode('utf-8') if isinstance(x, bytes) else x
+
+        sys_info[_hsa_agents_count] = len(hsa.agents)
+        agents = []
+        for i, agent in enumerate(hsa.agents):
+            agents += {
+                'Agent id': i,
+                'Vendor': decode(agent.vendor_name),
+                'Name': decode(agent.name),
+                'Type': agent.device,
+            }
+        sys_info[_hsa_agents] = agents
+
+        _dgpus = []
+        for a in hsa.agents:
+            if a.is_component and a.device == 'GPU':
+                _dgpus.append(decode(a.name))
+        sys_info[_hsa_gpus_count] = dgpu_count()
+        sys_info[_hsa_gpus] = ', '.join(_dgpus)
+    except Exception as e:
+        _warning_log.append(
+            "Warning (roc): No HSA Agents found, "
+            f"encountered exception when searching: {e}")
+
+    # SVML information
+    # Replicate some SVML detection logic from numba.__init__ here.
+    # If SVML load fails in numba.__init__ the splitting of the logic
+    # here will help diagnosing the underlying issue.
+    svml_lib_loaded = True
+    try:
+        if sys.platform.startswith('linux'):
+            llvmbind.load_library_permanently("libsvml.so")
+        elif sys.platform.startswith('darwin'):
+            llvmbind.load_library_permanently("libsvml.dylib")
+        elif sys.platform.startswith('win'):
+            llvmbind.load_library_permanently("svml_dispmd")
+        else:
+            svml_lib_loaded = False
+    except Exception:
+        svml_lib_loaded = False
+    func = getattr(llvmbind.targets, "has_svml", None)
+    sys_info[_llvm_svml_patched] = func() if func else False
+    sys_info[_svml_state] = config.USING_SVML
+    sys_info[_svml_loaded] = svml_lib_loaded
+    sys_info[_svml_operational] = all((
+        sys_info[_svml_state],
+        sys_info[_svml_loaded],
+        sys_info[_llvm_svml_patched],
+    ))
+
+    # Check which threading backends are available.
+    def parse_error(e, backend):
+        # parses a linux based error message, this is to provide feedback
+        # and hide user paths etc
+        try:
+            path, problem, symbol = [x.strip() for x in e.msg.split(':')]
+            extn_dso = os.path.split(path)[1]
+            if backend in extn_dso:
+                return "%s: %s" % (problem, symbol)
+        except Exception:
+            pass
+        return "Unknown import problem."
+
+    try:
+        from numba.np.ufunc import tbbpool  # NOQA
+        sys_info[_tbb_thread] = True
+    except ImportError as e:
+        # might be a missing symbol due to e.g. tbb libraries missing
+        sys_info[_tbb_thread] = False
+        sys_info[_tbb_error] = parse_error(e, 'tbbpool')
+
+    try:
+        from numba.np.ufunc import omppool
+        sys_info[_openmp_thread] = True
+        sys_info[_openmp_vendor] = omppool.openmp_vendor
+    except ImportError as e:
+        sys_info[_openmp_thread] = False
+        sys_info[_openmp_error] = parse_error(e, 'omppool')
+
+    try:
+        from numba.np.ufunc import workqueue  # NOQA
+        sys_info[_wkq_thread] = True
+    except ImportError as e:
+        sys_info[_wkq_thread] = True
+        sys_info[_wkq_error] = parse_error(e, 'workqueue')
+
+    # Look for conda and installed packages information
+    cmd = ('conda', 'info', '--json')
+    try:
+        conda_out = check_output(cmd)
+    except Exception as e:
+        _warning_log.append(f'Warning: Conda not available.\n Error was {e}\n')
+        # Conda is not available, try pip list to list installed packages
+        cmd = (sys.executable, '-m', 'pip', 'list')
+        try:
+            reqs = check_output(cmd)
+        except Exception as e:
+            _error_log.append(f'Error (pip): {e}')
+        else:
+            sys_info[_inst_pkg] = reqs.decode().splitlines()
+
+    else:
+        jsond = json.loads(conda_out.decode())
+        keys = {
+            'conda_build_version': _conda_build_ver,
+            'conda_env_version': _conda_env_ver,
+            'platform': _conda_platform,
+            'python_version': _conda_python_ver,
+            'root_writable': _conda_root_writable,
+        }
+        for conda_k, sysinfo_k in keys.items():
+            sys_info[sysinfo_k] = jsond.get(conda_k, 'N/A')
+
+        # Get info about packages in current environment
+        cmd = ('conda', 'list')
+        try:
+            conda_out = check_output(cmd)
+        except CalledProcessError as e:
+            _error_log.append(f'Error (conda): {e}')
+        else:
+            data = conda_out.decode().splitlines()
+            sys_info[_inst_pkg] = [l for l in data if not l.startswith('#')]
+
+    sys_info.update(get_os_spec_info(sys_info[_os_name]))
+    sys_info[_errors] = _error_log
+    sys_info[_warnings] = _warning_log
+    sys_info[_runtime] = (datetime.now() - sys_info[_start]).total_seconds()
+    return sys_info
+
+
+def display_sysinfo(info=None, sep_pos=45):
+    class DisplayMap(dict):
+        display_map_flag = True
+
+    class DisplaySeq(tuple):
+        display_seq_flag = True
+
+    class DisplaySeqMaps(tuple):
+        display_seqmaps_flag = True
+
+    if info is None:
+        info = get_sysinfo()
+
+    fmt = f'%-{sep_pos}s : %-s'
+    MB = 1024**2
+    template = (
+        ("-" * 80,),
+        ("__Time Stamp__",),
+        ("Report started (local time)", info.get(_start, '?')),
+        ("UTC start time", info.get(_start_utc, '?')),
+        ("Running time (s)", info.get(_runtime, '?')),
+        ("",),
+        ("__Hardware Information__",),
+        ("Machine", info.get(_machine, '?')),
+        ("CPU Name", info.get(_cpu_name, '?')),
+        ("CPU Count", info.get(_cpu_count, '?')),
+        ("Number of accessible CPUs", info.get(_cpus_allowed, '?')),
+        ("List of accessible CPUs cores", info.get(_cpus_list, '?')),
+        ("CFS Restrictions (CPUs worth of runtime)",
+            info.get(_cfs_restrict, 'None')),
+        ("",),
+        ("CPU Features", '\n'.join(
+            ' ' * (sep_pos + 3) + l if i else l
+            for i, l in enumerate(
+                textwrap.wrap(
+                    info.get(_cpu_features, '?'),
+                    width=79 - sep_pos
+                )
+            )
+        )),
+        ("",),
+        ("Memory Total (MB)", info.get(_mem_total, 0) // MB or '?'),
+        ("Memory Available (MB)"
+            if info.get(_os_name, '') != 'Darwin' or info.get(_psutil, False)
+            else "Free Memory (MB)", info.get(_mem_available, 0) // MB or '?'),
+        ("",),
+        ("__OS Information__",),
+        ("Platform Name", info.get(_platform_name, '?')),
+        ("Platform Release", info.get(_platform_release, '?')),
+        ("OS Name", info.get(_os_name, '?')),
+        ("OS Version", info.get(_os_version, '?')),
+        ("OS Specific Version", info.get(_os_spec_version, '?')),
+        ("Libc Version", info.get(_libc_version, '?')),
+        ("",),
+        ("__Python Information__",),
+        DisplayMap({k: v for k, v in info.items() if k.startswith('Python')}),
+        ("",),
+        ("__LLVM Information__",),
+        ("LLVM Version", info.get(_llvm_version, '?')),
+        ("",),
+        ("__CUDA Information__",),
+        ("CUDA Device Initialized", info.get(_cu_dev_init, '?')),
+        ("CUDA Driver Version", info.get(_cu_drv_ver, '?')),
+        ("CUDA Detect Output:",),
+        (info.get(_cu_detect_out, "None"),),
+        ("CUDA Librairies Test Output:",),
+        (info.get(_cu_lib_test, "None"),),
+        ("",),
+        ("__ROC information__",),
+        ("ROC Available", info.get(_roc_available, '?')),
+        ("ROC Toolchains", info.get(_roc_toolchains, []) or 'None'),
+        ("HSA Agents Count", info.get(_hsa_agents_count, 0)),
+        ("HSA Agents:",),
+        (DisplaySeqMaps(info.get(_hsa_agents, {})) or ('None',)),
+        ('HSA Discrete GPUs Count', info.get(_hsa_gpus_count, 0)),
+        ('HSA Discrete GPUs', info.get(_hsa_gpus, 'None')),
+        ("",),
+        ("__SVML Information__",),
+        ("SVML State, config.USING_SVML", info.get(_svml_state, '?')),
+        ("SVML Library Loaded", info.get(_svml_loaded, '?')),
+        ("llvmlite Using SVML Patched LLVM", info.get(_llvm_svml_patched, '?')),
+        ("SVML Operational", info.get(_svml_operational, '?')),
+        ("",),
+        ("__Threading Layer Information__",),
+        ("TBB Threading Layer Available", info.get(_tbb_thread, '?')),
+        ("+-->TBB imported successfully." if info.get(_tbb_thread, '?')
+            else f"+--> Disabled due to {info.get(_tbb_error, '?')}",),
+        ("OpenMP Threading Layer Available", info.get(_openmp_thread, '?')),
+        (f"+-->Vendor: {info.get(_openmp_vendor, '?')}"
+            if info.get(_openmp_thread, False)
+            else f"+--> Disabled due to {info.get(_openmp_error, '?')}",),
+        ("Workqueue Threading Layer Available", info.get(_wkq_thread, '?')),
+        ("+-->Workqueue imported successfully." if info.get(_wkq_thread, False)
+            else f"+--> Disabled due to {info.get(_wkq_error, '?')}",),
+        ("",),
+        ("__Numba Environment Variable Information__",),
+        (DisplayMap(info.get(_numba_env_vars, {})) or ('None found.',)),
+        ("",),
+        ("__Conda Information__",),
+        (DisplayMap({k: v for k, v in info.items()
+                     if k.startswith('Conda')}) or ("Conda not available.",)),
+        ("",),
+        ("__Installed Packages__",),
+        DisplaySeq(info.get(_inst_pkg, ("Couldn't retrieve packages info.",))),
+        ("",),
+        ("__Error log__" if info.get(_errors, [])
+            else "No errors reported.",),
+        DisplaySeq(info.get(_errors, [])),
+        ("",),
+        ("__Warning log__" if info.get(_warnings, [])
+            else "No warnings reported.",),
+        DisplaySeq(info.get(_warnings, [])),
+        ("-" * 80,),
+        ("If requested, please copy and paste the information between\n"
+         "the dashed (----) lines, or from a given specific section as\n"
+         "appropriate.\n\n"
+         "=============================================================\n"
+         "IMPORTANT: Please ensure that you are happy with sharing the\n"
+         "contents of the information present, any information that you\n"
+         "wish to keep private you should remove before sharing.\n"
+         "=============================================================\n",),
+    )
+    for t in template:
+        if hasattr(t, 'display_seq_flag'):
+            print(*t, sep='\n')
+        elif hasattr(t, 'display_map_flag'):
+            print(*tuple(fmt % (k, v) for (k, v) in t.items()), sep='\n')
+        elif hasattr(t, 'display_seqmaps_flag'):
+            for d in t:
+                print(*tuple(fmt % ('\t' + k, v) for (k, v) in d.items()),
+                      sep='\n', end='\n')
+        elif len(t) == 2:
+            print(fmt % t)
+        else:
+            print(*t)
+
+
+if __name__ == '__main__':
+    display_sysinfo()


Test patch:
diff --git a/numba/tests/test_cli.py b/numba/tests/test_cli.py
--- a/numba/tests/test_cli.py
+++ b/numba/tests/test_cli.py
@@ -4,9 +4,12 @@
 import subprocess
 import sys
 import threading
+import json
+from tempfile import TemporaryDirectory
 
 import unittest
 from numba.tests.support import TestCase
+import numba.misc.numba_sysinfo as nsi
 
 
 def run_cmd(cmdline, env=os.environ, timeout=60):
@@ -43,6 +46,61 @@ def test_as_module(self):
         o, _ = run_cmd(cmdline)
         self.assertIn("System info", o)
 
+    def test_json_sysinfo(self):
+        with TemporaryDirectory() as d:
+            path = os.path.join(d, "test_json_sysinfo.json")
+            cmdline = [sys.executable, "-m", "numba", "--sys-json", path]
+            run_cmd(cmdline)
+            with self.subTest(msg=f"{path} exists"):
+                self.assertTrue(os.path.exists(path))
+            with self.subTest(msg="json load"):
+                with open(path, 'r') as f:
+                    info = json.load(f)
+            safe_contents = {
+                int: (
+                    nsi._cpu_count,
+                ),
+                float: (
+                    nsi._runtime,
+                ),
+                str: (
+                    nsi._start,
+                    nsi._start_utc,
+                    nsi._machine,
+                    nsi._cpu_name,
+                    nsi._platform_name,
+                    nsi._os_name,
+                    nsi._os_version,
+                    nsi._python_comp,
+                    nsi._python_impl,
+                    nsi._python_version,
+                    nsi._llvm_version,
+                ),
+                bool: (
+                    nsi._cu_dev_init,
+                    nsi._roc_available,
+                    nsi._svml_state,
+                    nsi._svml_loaded,
+                    nsi._svml_operational,
+                    nsi._llvm_svml_patched,
+                    nsi._tbb_thread,
+                    nsi._openmp_thread,
+                    nsi._wkq_thread,
+                ),
+                list: (
+                    nsi._roc_toolchains,
+                    nsi._errors,
+                    nsi._warnings,
+                ),
+                dict: (
+                    nsi._numba_env_vars,
+                ),
+            }
+            for t, keys in safe_contents.items():
+                for k in keys:
+                    with self.subTest(k=k):
+                        self.assertIsInstance(info[k], t)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/numba/tests/test_sysinfo.py b/numba/tests/test_sysinfo.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_sysinfo.py
@@ -0,0 +1,189 @@
+import platform
+import unittest
+from unittest import skipUnless
+from unittest.mock import NonCallableMock
+from itertools import chain
+from datetime import datetime
+from contextlib import redirect_stdout
+from io import StringIO
+
+from numba.tests.support import TestCase
+import numba.misc.numba_sysinfo as nsi
+
+
+class TestSysInfo(TestCase):
+
+    def setUp(self):
+        super(TestSysInfo, self).setUp()
+        self.info = nsi.get_sysinfo()
+        self.safe_contents = {
+            int: (
+                nsi._cpu_count,
+            ),
+            float: (
+                nsi._runtime,
+            ),
+            str: (
+                nsi._machine,
+                nsi._cpu_name,
+                nsi._platform_name,
+                nsi._os_name,
+                nsi._os_version,
+                nsi._python_comp,
+                nsi._python_impl,
+                nsi._python_version,
+                nsi._llvm_version,
+            ),
+            bool: (
+                nsi._cu_dev_init,
+                nsi._roc_available,
+                nsi._svml_state,
+                nsi._svml_loaded,
+                nsi._svml_operational,
+                nsi._llvm_svml_patched,
+                nsi._tbb_thread,
+                nsi._openmp_thread,
+                nsi._wkq_thread,
+            ),
+            list: (
+                nsi._roc_toolchains,
+                nsi._errors,
+                nsi._warnings,
+            ),
+            dict: (
+                nsi._numba_env_vars,
+            ),
+            datetime: (
+                nsi._start,
+                nsi._start_utc,
+            ),
+        }
+        self.safe_keys = chain(*self.safe_contents.values())
+
+    def tearDown(self):
+        super(TestSysInfo, self).tearDown()
+        # System info might contain long strings or lists so delete it.
+        del self.info
+
+    def test_has_safe_keys(self):
+        for k in self.safe_keys:
+            with self.subTest(k=k):
+                self.assertIn(k, self.info)
+
+    def test_safe_content_type(self):
+        for t, keys in self.safe_contents.items():
+            for k in keys:
+                with self.subTest(k=k):
+                    self.assertIsInstance(self.info[k], t)
+
+    def test_has_no_error(self):
+        self.assertFalse(self.info[nsi._errors])
+
+    def test_display_empty_info(self):
+        output = StringIO()
+        with redirect_stdout(output):
+            res = nsi.display_sysinfo({})
+        self.assertIsNone(res)
+        output.close()
+
+
+class TestSysInfoWithPsutil(TestCase):
+
+    mem_total = 2 * 1024 ** 2  # 2_097_152
+    mem_available = 1024 ** 2  # 1_048_576
+    cpus_list = [1, 2]
+
+    def setUp(self):
+        super(TestSysInfoWithPsutil, self).setUp()
+        self.psutil_orig_state = nsi._psutil_import
+        # Mocking psutil
+        nsi._psutil_import = True
+        nsi.psutil = NonCallableMock()
+        vm = nsi.psutil.virtual_memory.return_value
+        vm.total = self.mem_total
+        vm.available = self.mem_available
+        if platform.system() in ('Linux', 'Windows',):
+            # cpu_affiniy only available on Linux and Windows
+            proc = nsi.psutil.Process.return_value
+            proc.cpu_affinity.return_value = self.cpus_list
+        else:
+            nsi.psutil.Process.return_value = None
+
+        self.info = nsi.get_os_spec_info(platform.system())
+
+    def tearDown(self):
+        super(TestSysInfoWithPsutil, self).tearDown()
+        nsi._psutil_import = self.psutil_orig_state
+
+    def test_has_all_data(self):
+        keys = (nsi._mem_total, nsi._mem_available)
+        for k in keys:
+            with self.subTest(k=k):
+                self.assertIn(k, self.info.keys())
+                self.assertIsInstance(self.info[k], int)
+
+    def test_has_correct_values(self):
+        self.assertEqual(self.info[nsi._mem_total], self.mem_total)
+        self.assertEqual(self.info[nsi._mem_available], self.mem_available)
+
+    @skipUnless(platform.system() in ('Linux', 'Windows'),
+                "CPUs allowed info only available on Linux and Windows")
+    def test_cpus_list(self):
+        self.assertEqual(self.info[nsi._cpus_allowed], len(self.cpus_list))
+        self.assertEqual(self.info[nsi._cpus_list],
+                         ' '.join(str(n) for n in self.cpus_list))
+
+
+class TestSysInfoWithoutPsutil(TestCase):
+
+    def setUp(self):
+        super(TestSysInfoWithoutPsutil, self).setUp()
+        self.psutil_orig_state = nsi._psutil_import
+        nsi._psutil_import = False
+        self.info = nsi.get_os_spec_info(platform.system())
+
+    def tearDown(self):
+        super(TestSysInfoWithoutPsutil, self).tearDown()
+        nsi._psutil_import = self.psutil_orig_state
+
+    def test_has_all_data(self):
+        keys = (nsi._mem_total, nsi._mem_available)
+        for k in keys:
+            with self.subTest(k=k):
+                self.assertIn(k, self.info.keys())
+                self.assertIsInstance(self.info[k], int)
+
+
+class TestPlatformSpecificInfo(TestCase):
+
+    def setUp(self):
+        self.plat_spec_info = {
+            'Linux': {
+                str: (nsi._libc_version,),
+            },
+            'Windows': {
+                str: (nsi._os_spec_version,),
+            },
+            'Darwin': {
+                str: (nsi._os_spec_version,),
+            },
+        }
+        self.os_name = platform.system()
+        self.contents = self.plat_spec_info.get(self.os_name, {})
+        self.info = nsi.get_os_spec_info(self.os_name)
+
+    def test_has_all_data(self):
+        keys = chain(*self.contents.values())
+        for k in keys:
+            with self.subTest(k=k):
+                self.assertIn(k, self.info.keys())
+
+    def test_content_type(self):
+        for t, keys in self.contents.items():
+            for k in keys:
+                with self.subTest(k=k):
+                    self.assertIsInstance(self.info[k], t)
+
+
+if __name__ == '__main__':
+    unittest.main()