packages feed

mem-info-0.4.0.0: test/MemInfo/MemInfoSpec.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : MemInfo.SysInfoSpec
Copyright   : (c) 2023 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3
-}
module MemInfo.MemInfoSpec (spec) where

import Data.GenValidity (GenValid (..))
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Word (Word16)
import Fmt (blockMapF, build, fmt, (+|), (|+))
import MemInfo.Files.Root (initRoot, useTmp, writeRootedFile)
import MemInfo.Files.Smap (genBaseSmap)
import MemInfo.OrphanInstances ()
import System.Directory (createDirectoryIfMissing, createFileLink)
import System.FilePath (takeDirectory, (</>))
import System.MemInfo (readForOnePid', readMemUsage)
import System.MemInfo.Proc (MemUsage, amass)
import System.MemInfo.SysInfo (
  KernelVersion,
  mkReportBud,
 )
import Test.Hspec
import Test.QuickCheck
import Test.QuickCheck.Monadic (PropertyM, assert, monadicIO, pick, run)


spec :: Spec
spec = do
  describe "module System.MemInfo.MemProc" $ around useTmp $ do
    context "the proc fs stores mem info in <proc>/<id>/smap" $ do
      context "and has 1 non-root process" $ do
        context "readForOnePid'" $ do
          it "should generate the correct report" prop_ReadOneSmapProcPid
        context "readMemUsage'" $ do
          it "should generate the correct report" prop_ReadSmapProcMemUsage


prop_ReadSmapProcMemUsage :: FilePath -> Property
prop_ReadSmapProcMemUsage root = monadicIO $ do
  let fakeProc = "/usr/bin/true"
  (wantedUsage, smapsTxt) <- pick genBaseSmap
  let asReport = amass True [(fakeProc, wantedUsage)]
      writeFiles = writeSmapProcFiles root smapsTxt fakeProc
  verifyReadMemUsage root asReport gen3xKernel writeFiles


verifyReadMemUsage ::
  FilePath ->
  Map Text MemUsage ->
  Gen KernelVersion ->
  (Word16 -> IO ()) ->
  PropertyM IO ()
verifyReadMemUsage root expected genKernel writeFiles = do
  thePid <- pick genValidProcId
  version <- pick genKernel
  -- some matches are incomplete, that's ok, the test should fail if they fai
  Right procMap <- run $ do
    initRoot root version
    writeFiles thePid
    Just bud <- mkReportBud root (fromIntegral thePid :| [])
    readMemUsage bud
  assert (expected == procMap)


prop_ReadOneSmapProcPid :: FilePath -> Property
prop_ReadOneSmapProcPid root = monadicIO $ do
  let fakeProc = "/usr/bin/true"
  (wantedUsage, smapsTxt) <- pick genBaseSmap
  let asReport = amass True [(fakeProc, wantedUsage)]
      writeFiles = writeSmapProcFiles root smapsTxt fakeProc
  verifyReadForOnePid root asReport gen3xKernel writeFiles


verifyReadForOnePid ::
  FilePath ->
  Map Text MemUsage ->
  Gen KernelVersion ->
  (Word16 -> IO ()) ->
  PropertyM IO ()
verifyReadForOnePid root expected genKernel writeFiles = do
  thePid <- pick genValidProcId
  version <- pick genKernel
  -- this match is incomplete, that's ok, the test fails if the run produces a Left
  Right (theProc, theUsage) <- run $ do
    initRoot root version
    writeFiles thePid
    readForOnePid' root $ fromIntegral thePid
  assert (Map.lookup theProc expected == Just theUsage)


gen3xKernel :: Gen KernelVersion
gen3xKernel = do
  patch <- genValid
  minor <- genValid
  pure (3, minor, patch)


writeSmapProcFiles :: FilePath -> Text -> Text -> Word16 -> IO ()
writeSmapProcFiles root txt procName thePid = do
  let fakeParent = "/sbin/fake"
  writeRootedExeAndCmdLine root fakeParent 1
  writeRootedExeAndCmdLine root procName thePid
  writeRootedFakeStatus root procName thePid 1
  writeRootedFile root ("" +| thePid |+ "/smaps") txt


genValidProcId :: Gen Word16
genValidProcId = genValid `suchThat` (> 1)


writeRootedExeAndCmdLine :: FilePath -> Text -> Word16 -> IO ()
writeRootedExeAndCmdLine root fakeProc procId = do
  writeRootedFile root ("" +| procId |+ "/cmdline") fakeProc
  writeRootedExeLink root ("" +| procId |+ "/exe") fakeProc


writeRootedFakeStatus :: FilePath -> Text -> Word16 -> Word16 -> IO ()
writeRootedFakeStatus root fakeProc procId parentId = do
  let txt = fmt $ blockMapF $ statusInfoFields fakeProc parentId
      statusPath = "" +| procId |+ "/status"
  writeRootedFile root statusPath txt


statusInfoFields :: Text -> Word16 -> [(Text, Text)]
statusInfoFields fakeProc parentId =
  [ ("Name", fakeProc)
  , ("PPid", fmt $ build $ toInteger parentId)
  ]


writeRootedExeLink :: FilePath -> FilePath -> Text -> IO ()
writeRootedExeLink root path link = do
  let target = root </> path
  createDirectoryIfMissing True $ takeDirectory target
  createFileLink (Text.unpack link) target