Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
PedramBakh committed Mar 7, 2024
1 parent 2d2b34d commit 992072e
Show file tree
Hide file tree
Showing 32 changed files with 2,939 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Publish to PyPI
name: Build

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Test, Lint, and Format
name: Unit Tests

on:
push:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,12 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/

# VS Code
.vscode/
.vscode

# macOS
.DS_Store
/tests/internal/
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# **carbontracker**
[![pypi](https://img.shields.io/pypi/v/carbontracker?label=pypi)](https://pypi.org/project/carbontracker/)
[![Build](https://github.com/lfwa/carbontracker/actions/workflows/publish.yml/badge.svg)](https://github.com/lfwa/carbontracker/actions)
[![PyPI](https://img.shields.io/pypi/v/carbontracker?label=PyPI)](https://pypi.org/project/carbontracker/)
[![Python](https://img.shields.io/badge/python-%3E%3D3.7-blue)](https://www.python.org/downloads/)
[![build](https://github.com/lfwa/carbontracker/workflows/build/badge.svg)](https://github.com/lfwa/carbontracker/actions)
[![Unit Tests](https://github.com/lfwa/carbontracker/actions/workflows/test.yml/badge.svg)](https://github.com/lfwa/carbontracker/actions)
[![License](https://img.shields.io/github/license/lfwa/carbontracker)](https://github.com/lfwa/carbontracker/blob/master/LICENSE)


## About
**carbontracker** is a tool for tracking and predicting the energy consumption and carbon footprint of training deep learning models as described in [Anthony et al. (2020)](https://arxiv.org/abs/2007.03051).

Expand Down
3 changes: 0 additions & 3 deletions carbontracker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import subprocess
from carbontracker.tracker import CarbonTracker
import ast
import sys


def main():
# Create a parser for the known arguments
Expand Down Expand Up @@ -36,4 +34,3 @@ def main():

if __name__ == "__main__":
main()

4 changes: 4 additions & 0 deletions carbontracker/components/cpu/intel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@


class IntelCPU(Handler):
def __init__(self, pids, devices_by_pid):
super().__init__(pids, devices_by_pid)
self._handler = None

def devices(self):
"""Returns the name of all RAPL Domains"""
return self._devices
Expand Down
7 changes: 6 additions & 1 deletion carbontracker/components/gpu/nvidia.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@


class NvidiaGPU(Handler):
def __init__(self, pids, devices_by_pid):
super().__init__(pids, devices_by_pid)
self._handles = None

def devices(self):
"""
Note:
Expand Down Expand Up @@ -69,6 +73,7 @@ def init(self):

def shutdown(self):
pynvml.nvmlShutdown()
self._handles = None

def _get_handles(self):
"""Returns handles of GPUs in slurm job if existent otherwise all
Expand Down Expand Up @@ -114,7 +119,7 @@ def _get_handles_by_pid(self):
gpu_pids = [
p.pid
for p in pynvml.nvmlDeviceGetComputeRunningProcesses(handle)
+ pynvml.nvmlDeviceGetGraphicsRunningProcesses(handle)
+ pynvml.nvmlDeviceGetGraphicsRunningProcesses(handle)
]

if set(gpu_pids).intersection(self.pids):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import requests
import numpy as np
import time

from carbontracker import exceptions
from carbontracker.emissions.intensity.fetcher import IntensityFetcher
Expand Down
7 changes: 3 additions & 4 deletions carbontracker/emissions/intensity/intensity.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os.path
import traceback

import geocoder
import pkg_resources
import importlib.resources
import numpy as np
import pandas as pd

Expand All @@ -12,7 +13,6 @@
from carbontracker.emissions.intensity.fetchers import energidataservice
from carbontracker.emissions.intensity.fetchers import electricitymaps


def get_default_intensity():
"""Retrieve static default carbon intensity value based on location."""
try:
Expand All @@ -27,8 +27,7 @@ def get_default_intensity():

try:
carbon_intensities_df = pd.read_csv(
pkg_resources.resource_filename("carbontracker", "data/carbon-intensities.csv")
)
str(importlib.resources.files("carbontracker").joinpath("data", "carbon-intensities.csv")))
intensity_row = carbon_intensities_df[carbon_intensities_df["alpha-2"] == country].iloc[0]
intensity = intensity_row["Carbon intensity of electricity (gCO2/kWh)"]
year = intensity_row["Year"]
Expand Down
2 changes: 1 addition & 1 deletion carbontracker/loggerutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def err_info(self, msg):
self.logger_err.info(msg)

def err_warn(self, msg):
self.logger_err.warn(msg)
self.logger_err.warning(msg)

def err_critical(self, msg):
self.logger_err.critical(msg)
2 changes: 1 addition & 1 deletion carbontracker/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ def stop(self):
if not self.running:
return

self.measuring = False
self.running = False
self.logger.info("Monitoring thread ended.")
self.logger.output("Finished monitoring.", verbose_level=1)


def epoch_start(self):
self.epoch_counter += 1
self.cur_epoch_time = time.time()
Expand Down
Empty file added tests/components/__init__.py
Empty file.
102 changes: 102 additions & 0 deletions tests/components/test_apple_silicon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import unittest
from unittest.mock import patch
from carbontracker.components.apple_silicon.powermetrics import AppleSiliconCPU, AppleSiliconGPU, PowerMetricsUnified


class TestAppleSiliconCPU(unittest.TestCase):
def setUp(self):
self.cpu_handler = AppleSiliconCPU(pids=[], devices_by_pid={})
self.cpu_handler.init()

def test_shutdown(self):
self.cpu_handler.shutdown()

@patch('platform.system', return_value="Darwin")
def test_available_darwin(self, mock_platform):
self.assertTrue(self.cpu_handler.available())

@patch('platform.system', return_value="AlienOS")
def test_available_not_darwin(self, mock_platform):
self.assertFalse(self.cpu_handler.available())

def test_devices(self):
self.assertEqual(self.cpu_handler.devices(), ["CPU"])

@patch('carbontracker.components.apple_silicon.powermetrics.PowerMetricsUnified.get_output',
return_value="CPU Power: 1000 mW")
def test_power_usage_with_match(self, mock_get_output):
self.assertEqual(self.cpu_handler.power_usage(), 1.0)

@patch('carbontracker.components.apple_silicon.powermetrics.PowerMetricsUnified.get_output',
return_value="No CPU Power data")
def test_power_usage_no_match(self, mock_get_output):
self.assertEqual(self.cpu_handler.power_usage(), 0.0)


class TestAppleSiliconGPU(unittest.TestCase):
def setUp(self):
self.gpu_handler = AppleSiliconGPU(pids=[], devices_by_pid={})
self.gpu_handler.init()

@patch('platform.system', return_value="Darwin")
def test_available_darwin(self, mock_platform):
self.assertTrue(self.gpu_handler.available())

@patch('platform.system', return_value="Windows")
def test_available_not_darwin(self, mock_platform):
self.assertFalse(self.gpu_handler.available())

def test_devices(self):
self.assertEqual(self.gpu_handler.devices(), ["GPU", "ANE"])

@patch('carbontracker.components.apple_silicon.powermetrics.PowerMetricsUnified.get_output',
return_value="GPU Power: 500 mW\nANE Power: 300 mW")
def test_power_usage_with_match(self, mock_get_output):
self.assertAlmostEqual(self.gpu_handler.power_usage(), 0.8, places=2)

@patch('carbontracker.components.apple_silicon.powermetrics.PowerMetricsUnified.get_output',
return_value="No GPU Power data")
def test_power_usage_no_match(self, mock_get_output):
self.assertEqual(self.gpu_handler.power_usage(), 0.0)


class TestPowerMetricsUnified(unittest.TestCase):
@patch('subprocess.check_output', return_value="Sample Output")
@patch('time.time', side_effect=[100, 101, 102, 200, 202])
def test_get_output_with_actual_call(self, mock_time, mock_check_output):
# First call - should call subprocess
output1 = PowerMetricsUnified.get_output()

# Second call - should use cached output
output2 = PowerMetricsUnified.get_output()

# Advance time to invalidate cache
PowerMetricsUnified._last_updated -= 2

# Third call - should call subprocess again
output3 = PowerMetricsUnified.get_output()

self.assertEqual(mock_check_output.call_count, 2)
self.assertEqual(output1, "Sample Output")
self.assertEqual(output2, "Sample Output")
self.assertEqual(output3, "Sample Output")


class TestAppleSiliconGPUPowerUsage(unittest.TestCase):
def setUp(self):
self.gpu_handler = AppleSiliconGPU(pids=[], devices_by_pid={})
self.gpu_handler.init()

@patch('carbontracker.components.apple_silicon.powermetrics.PowerMetricsUnified.get_output',
return_value="GPU Power: 500 mW\nANE Power: 300 mW")
def test_power_usage_with_match(self, mock_get_output):
self.assertAlmostEqual(self.gpu_handler.power_usage(), 0.8, places=2)

@patch('carbontracker.components.apple_silicon.powermetrics.PowerMetricsUnified.get_output',
return_value="No GPU Power data")
def test_power_usage_no_match(self, mock_get_output):
self.assertEqual(self.gpu_handler.power_usage(), 0.0)


if __name__ == '__main__':
unittest.main()
141 changes: 141 additions & 0 deletions tests/components/test_intel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import unittest
from unittest.mock import patch, mock_open
from carbontracker.components.cpu.intel import IntelCPU
from carbontracker.components.component import Component
from carbontracker import exceptions
import re

class TestIntelCPU(unittest.TestCase):
@patch("os.path.exists")
@patch("os.listdir")
def test_available(self, mock_listdir, mock_exists):
mock_exists.return_value = True
mock_listdir.return_value = ["some_directory"]

component = Component(name='cpu', pids=[], devices_by_pid={})
self.assertTrue(component.available())

@patch("os.path.exists")
@patch("os.listdir")
@patch("builtins.open", new_callable=mock_open, read_data="some_name")
def test_devices(self, mock_file, mock_listdir, mock_exists):
mock_exists.return_value = True
mock_listdir.side_effect = [["intel-rapl:0", "intel-rapl:1"], ["name"], ["name"]]

cpu = IntelCPU(pids=[], devices_by_pid={})
cpu.init()

self.assertEqual(cpu.devices(), ["cpu:0", "cpu:1"])

@patch("os.path.exists")
@patch("os.listdir")
@patch.object(Component, "available", return_value=False)
def test_available_false(self, mock_available, mock_listdir, mock_exists):
mock_exists.return_value = False
mock_listdir.return_value = []

cpu = Component(name='cpu', pids=[], devices_by_pid={})
self.assertFalse(cpu.available())

@patch("time.sleep")
@patch("carbontracker.components.cpu.intel.IntelCPU._get_measurements")
def test_power_usage_positive(self, mock_get_measurements, mock_sleep):
mock_get_measurements.side_effect = [[10, 20], [20, 30]]
mock_sleep.return_value = None

cpu = IntelCPU(pids=[], devices_by_pid={})
power_usages = cpu.power_usage()

self.assertEqual(power_usages, [0.00001, 0.00001])

@patch("time.sleep")
@patch("carbontracker.components.cpu.intel.IntelCPU._get_measurements")
def test_power_usage_negative(self, mock_get_measurements, mock_sleep):
mock_get_measurements.side_effect = [[30, 20], [20, 30]]
mock_sleep.return_value = None

cpu = IntelCPU(pids=[], devices_by_pid={})
cpu._devices = ["cpu:0", "cpu:1"]
power_usages = cpu.power_usage()

self.assertEqual(power_usages, [0.00, 0.00])


@patch("builtins.open", new_callable=mock_open, read_data="1000000")
def test__read_energy(self, mock_file):
cpu = IntelCPU(pids=[], devices_by_pid={})
energy = cpu._read_energy("/some/path")
self.assertEqual(energy, 1000000)

@patch("os.path.exists")
@patch("os.listdir")
@patch("builtins.open", new_callable=mock_open)
def test__get_measurements(self, mock_file, mock_listdir, mock_exists):
mock_exists.return_value = True
mock_listdir.return_value = ["intel-rapl:0", "intel-rapl:1"]
mock_file.return_value.read.return_value = "1000000"

cpu = IntelCPU(pids=[], devices_by_pid={})
cpu.init()

measurements = cpu._get_measurements()
self.assertEqual(measurements, [1000000, 1000000])

@patch("os.listdir")
@patch("builtins.open", new_callable=mock_open, read_data="cpu")
def test__convert_rapl_name(self, mock_file, mock_listdir):
mock_listdir.return_value = ["intel-rapl:0", "intel-rapl:1"]

cpu = IntelCPU(pids=[], devices_by_pid={})
cpu.init()

self.assertEqual(cpu._convert_rapl_name("intel-rapl:0", re.compile("intel-rapl:.")), "cpu:0")

@patch("os.path.exists")
@patch("os.listdir")
@patch("builtins.open", new_callable=mock_open, read_data="cpu")
def test_init(self, mock_file, mock_listdir, mock_exists):
mock_exists.return_value = True
mock_listdir.return_value = ["intel-rapl:0", "intel-rapl:1"]

cpu = IntelCPU(pids=[], devices_by_pid={})
cpu.init()

self.assertEqual(cpu.devices(), ["cpu:0", "cpu:1"])

@patch("os.path.join")
@patch("os.listdir")
@patch("carbontracker.components.cpu.intel.IntelCPU._read_energy")
def test__get_measurements_permission_error(self, mock_read_energy, mock_listdir, mock_path_join):
mock_path_join.return_value = "/some/path"
mock_read_energy.side_effect = PermissionError()

cpu = IntelCPU(pids=[], devices_by_pid={})
cpu._rapl_devices = ["device1"]
with self.assertRaises(exceptions.IntelRaplPermissionError):
cpu._get_measurements()

@patch("os.path.join")
@patch("os.listdir")
@patch("carbontracker.components.cpu.intel.IntelCPU._read_energy")
def test__get_measurements_file_not_found(self, mock_read_energy, mock_listdir, mock_path_join):
mock_path_join.return_value = "/some/path"
mock_read_energy.side_effect = [FileNotFoundError(), 1000000, 1000000, 1000000]
mock_listdir.return_value = ["intel-rapl:0", "intel-rapl:1"]

cpu = IntelCPU(pids=[], devices_by_pid={})
cpu._rapl_devices = ["intel-rapl:0", "intel-rapl:1"]
cpu.parts_pattern = re.compile(r"intel-rapl:.")
measurements = cpu._get_measurements()

self.assertEqual(measurements, [2000000, 1000000])


def test_shutdown(self):
cpu = IntelCPU(pids=[], devices_by_pid={})
# As the shutdown method is currently a pass, there's nothing to assert here.
# But we still call it for the sake of completeness and future modifications.
cpu.shutdown()

if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit 992072e

Please sign in to comment.