diff --git a/.vscode/launch.json b/.vscode/launch.json index 5abfba7..392334e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,8 @@ "kernel", "0x10", "0x10", "rootfs", "0x40", "0xDD", "dtb", "0xCC", "0xCC", - "-o", "doc/example/report.md" + "-o", "doc/example/report.md", + "-s", "3" ], "justMyCode": false } diff --git a/doc/example/report.md b/doc/example/report.md index cc1e074..ff16297 100644 --- a/doc/example/report.md +++ b/doc/example/report.md @@ -1,8 +1,8 @@ ![memory map diagram](report.png) |name|origin|size|remaining|collisions |:-|:-|:-|:-|:-| -|kernel|0x10|0x50|-0x10|{'rootfs': '0x50'}| -|uboot|0xD0|0x50|-0x10|{'uboot-scr': '0x110'}| -|rootfs|0x50|0x30|0x10|{'kernel': '0x50'}| -|dtb|0x90|0x30|0x10|{}| -|uboot-scr|0x110|0x30|0x50|{'uboot': '0x110'}| +|kernel|0x10|0x50|-0x10|{'rootfs': '0x50'}| +|uboot|0xD0|0x50|-0x10|{'uboot-scr': '0x110'}| +|rootfs|0x50|0x30|0x10|{'kernel': '0x50'}| +|dtb|0x90|0x30|0x10|{}| +|uboot-scr|0x110|0x30|0x50|{'uboot': '0x110'}| diff --git a/doc/example/report.png b/doc/example/report.png index 4e11e46..aef8044 100644 Binary files a/doc/example/report.png and b/doc/example/report.png differ diff --git a/mm/diagram.py b/mm/diagram.py index e92a0db..49543c6 100644 --- a/mm/diagram.py +++ b/mm/diagram.py @@ -25,21 +25,29 @@ @typeguard.typechecked class MemoryMap: - height: int = 400 - """height of the diagram image""" - width: int = 400 - """width of the diagram image""" - bgcolour = "oldlace" - def __init__(self): - self._legend_width = 100 - """width of the area used for text annotations/legend""" + self.bgcolour = "oldlace" + + self.height: int = 400 + """height of the diagram image""" + self.width: int = 400 + """width of the diagram image""" + + self.default_region_text_size: int = 12 + self.fixed_legend_text_size = 12 + self._region_list = None """List of region objects""" # create a list of region objects populated with input data self._region_list = self._process_input() + self.scale_factor: int = self.args.scale + self._rescale() + + self._legend_width = self.width // 2 + """width of the area used for text annotations/legend""" + # temporarily sort by ascending origin attribute and assign the draw indent self._region_list.sort(key=lambda x: x.origin, reverse=False) region_indent = 0 @@ -50,40 +58,50 @@ def __init__(self): # sort in descending order so largest regions are drawn first in z-order (background) self._region_list.sort(key=lambda x: x.size, reverse=True) + self._generate() + + def _generate(self): # output image diagram self._create_diagram(self._region_list) # output markdown report (refs image) self._create_markdown(self._region_list) + def _rescale(self): + self.height = self.height * self.scale_factor + self.width = self.width * self.scale_factor + # self.default_region_text_size = self.default_region_text_size * self.scale_factor + def _create_diagram(self, region_list: List[mm.types.MemoryRegion]): # init the main image - img_main = PIL.Image.new("RGB", (MemoryMap.width, MemoryMap.height), color=MemoryMap.bgcolour) + img_main = PIL.Image.new("RGB", (self.width, self.height), color=self.bgcolour) - # add a new layer (region_img) for each region block - # to the main image object (img_main) + # paste each new graphic element image to main image for region in region_list: - region.create_img(img_width=MemoryMap.width - self._legend_width) + # Regions + region.create_img(img_width=(self.width - self._legend_width), font_size=self.default_region_text_size) if not region.img: continue img_main.paste(region.img, (self._legend_width + region.draw_indent, region.origin), region.img) # Origin Address Text - origin_text_label = mm.types.TextLabel(region._origin, 8) + origin_text_label = mm.types.TextLabel(region._origin, self.fixed_legend_text_size) img_main.paste(origin_text_label.img, (0, region.origin - origin_text_label.height + 1)) # Region End Address Text end_addr_val = region.origin + region.size - end_addr_text_label = mm.types.TextLabel(hex(end_addr_val).upper(), 8) + end_addr_text_label = mm.types.TextLabel(hex(end_addr_val), self.fixed_legend_text_size) img_main.paste(end_addr_text_label.img, (0, end_addr_val - end_addr_text_label.height + 1)) + # Dash Lines from text to region line_width = 1 line_canvas = PIL.ImageDraw.Draw(img_main) dash_gap = 4 dash_len = dash_gap / 2 - for x in range(40, 90, dash_gap): + for x in range(end_addr_text_label.width * 2, self._legend_width - 10, dash_gap): line_canvas.line((x, end_addr_val - line_width, x + dash_len, end_addr_val - line_width), fill="black", width=line_width) + for x in range(origin_text_label.width * 2, self._legend_width - 10, dash_gap): line_canvas.line((x, region.origin - line_width, x + dash_len, region.origin - line_width), fill="black", width=1) # rotate the entire diagram so the origin is at the bottom @@ -109,10 +127,12 @@ def _process_input(self) -> List[mm.types.MemoryRegion]: self.parser.add_argument("-o", "--out", help='path to the markdown output report file. Default: "out/report.md"', default="out/report.md") self.parser.add_argument("-l", "--limit", help="The maximum memory address for the diagram. Default: 400", default=400, type=int) + self.parser.add_argument("-s", "--scale", help="The scale factor for the diagram. Default: 1", default=1, type=int) + self.args = self.parser.parse_args() if self.args.limit: - MemoryMap.height = self.args.limit + self.height = self.args.limit if len(sys.argv) == 1: self.parser.error("must pass in data points") @@ -135,7 +155,7 @@ def _process_input(self) -> List[mm.types.MemoryRegion]: region_list.append(mm.types.MemoryRegion(name, origin, size)) for r in region_list: - r.calc_nearest_region(region_list) + r.calc_nearest_region(region_list, self.height) return region_list @@ -149,4 +169,5 @@ def _batched(self, iterable, n): if __name__ == "__main__": - MemoryMap() + d = MemoryMap() + diff --git a/mm/types.py b/mm/types.py index 406f38c..452c4d9 100644 --- a/mm/types.py +++ b/mm/types.py @@ -77,8 +77,8 @@ def _pick_available_colour(self): logging.debug(f"\t### {len(MemoryRegion._remaining_colours)} colours left ###") return chosen_colour_name - def calc_nearest_region(self, region_list: List['MemoryRegion']): - import mm.diagram + def calc_nearest_region(self, region_list: List['MemoryRegion'], diagram_height: int): + """Calculate the remaining number of bytes until next region block""" region_distances = {} logging.debug(f"Calculating nearest distances to {self.name} region:") @@ -132,15 +132,15 @@ def calc_nearest_region(self, region_list: List['MemoryRegion']): lowest = min(region_distances, key=region_distances.get) self.remain = hex(region_distances[lowest]) else: - self.remain = hex(mm.diagram.MemoryMap.height - this_region_end) + self.remain = hex(diagram_height - this_region_end) elif self.collisons and not self.remain: - self.remain = hex(mm.diagram.MemoryMap.height - this_region_end) + self.remain = hex(diagram_height - this_region_end) @typeguard.typechecked class MemoryRegion(Region): - def create_img(self, img_width: int): + def create_img(self, img_width: int, font_size: int): logging.info(self) if not self.size: @@ -160,7 +160,7 @@ def create_img(self, img_width: int): ) # draw name text - region_img.paste(TextLabel(text=self.name, font_size=7).img, (5, 5)) + region_img.paste(TextLabel(text=self.name, font_size=font_size).img, (5, 5)) self.img = region_img diff --git a/readme.md b/readme.md index 3331fcb..4c907c2 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ An example can be found in [doc/example/report.md](doc/example/report.md) ### Usage: ``` -usage: mm.diagram [-h] [-o OUT] [-l LIMIT] [regions ...] +usage: diagram.py [-h] [-o OUT] [-l LIMIT] [-s SCALE] [regions ...] positional arguments: regions command line input for regions should be tuples of name, origin and size. @@ -24,6 +24,8 @@ options: -o OUT, --out OUT path to the markdown output report file. Default: "out/report.md" -l LIMIT, --limit LIMIT The maximum memory address for the diagram. Default: 400 + -s SCALE, --scale SCALE + The scale factor for the diagram. Default: 1 ``` - Generate five regions called `kernel`, `rootfs`, `dtb`, `uboot` and `uboot-scr` where four of the five regions intersect/collide. The default report output path is used. Diagram output is shown at the top of the page. diff --git a/tests/test_args.py b/tests/test_args.py index d895c9e..0570901 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -3,6 +3,7 @@ import mm.diagram import pathlib + # Check the output report at /tmp/pytest/tests.test_args.md @@ -51,14 +52,20 @@ def test_invalid_out_arg(): def test_valid_default_out_arg(): ''' should create default report dir/files ''' + + report = pathlib.Path("out/report.md") + image = pathlib.Path("out/report.png") + report.unlink(missing_ok=True) + image.unlink(missing_ok=True) + with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'a', '0x10', '0x10']): mm.diagram.MemoryMap() - assert pathlib.Path("out/report.md").exists() - assert pathlib.Path("out/report.png").exists() + assert report.exists() + assert image.exists() def test_invalid_duplicate_name_arg(): @@ -77,13 +84,19 @@ def test_invalid_duplicate_name_arg(): def test_valid_custom_out_arg(): ''' should create custom report dir/files ''' + + report = pathlib.Path(f"/tmp/pytest/{__name__}.md") + image = pathlib.Path(f"/tmp/pytest/{__name__}.png") + report.unlink(missing_ok=True) + image.unlink(missing_ok=True) + with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'a', '0x10', '0x10', "-o", - f"/tmp/pytest/{__name__}.md"]): + str(report)]): mm.diagram.MemoryMap() - assert pathlib.Path(f"/tmp/pytest/{__name__}.md").exists() - assert pathlib.Path(f"/tmp/pytest/{__name__}.png").exists() + assert report.exists() + assert image.exists() diff --git a/tests/test_distance.py b/tests/test_distance.py index ce7c27d..b6e0f80 100644 --- a/tests/test_distance.py +++ b/tests/test_distance.py @@ -7,6 +7,7 @@ def test_distance_three_regions_same_size_no_collisions(): """ """ + diagram_height = 1000 with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'kernel', @@ -21,8 +22,7 @@ def test_distance_three_regions_same_size_no_collisions(): "-o", f"/tmp/pytest/{__name__}.md", "-l", - "1000"], - mm.diagram.MemoryMap.height, 1000): + str(diagram_height)]): d = mm.diagram.MemoryMap() for region in d._region_list: @@ -42,6 +42,7 @@ def test_distance_three_regions_same_size_no_collisions(): def test_distance_three_regions_touching_no_collisions(): """ """ + diagram_height = 1000 with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'kernel', @@ -56,8 +57,7 @@ def test_distance_three_regions_touching_no_collisions(): "-o", f"/tmp/pytest/{__name__}.md", "-l", - "1000"], - mm.diagram.MemoryMap.height, 1000): + str(diagram_height)]): d = mm.diagram.MemoryMap() for region in d._region_list: @@ -74,8 +74,10 @@ def test_distance_three_regions_touching_no_collisions(): assert region._size == "0x30" assert region.remain == "0x348" + def test_distance_three_regions_diff_size_no_collisions(): """ """ + diagram_height = 1000 with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'kernel', @@ -90,8 +92,7 @@ def test_distance_three_regions_diff_size_no_collisions(): "-o", f"/tmp/pytest/{__name__}.md", "-l", - "1000"], - mm.diagram.MemoryMap.height, 1000): + str(diagram_height)]): d = mm.diagram.MemoryMap() for region in d._region_list: if region.name == "kernel": @@ -110,6 +111,7 @@ def test_distance_three_regions_diff_size_no_collisions(): def test_distance_three_regions_bottom_collision(): """ """ + diagram_height = 1000 with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'kernel', @@ -124,8 +126,7 @@ def test_distance_three_regions_bottom_collision(): "-o", f"/tmp/pytest/{__name__}.md", "-l", - "1000"], - mm.diagram.MemoryMap.height, 1000): + str(diagram_height)]): d = mm.diagram.MemoryMap() for region in d._region_list: if region.name == "kernel": @@ -144,6 +145,7 @@ def test_distance_three_regions_bottom_collision(): def test_distance_three_regions_bottom_middle_collision(): """ """ + diagram_height = 1000 with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'kernel', @@ -158,8 +160,7 @@ def test_distance_three_regions_bottom_middle_collision(): "-o", f"/tmp/pytest/{__name__}.md", "-l", - "1000"], - mm.diagram.MemoryMap.height, 1000): + str(diagram_height)]): d = mm.diagram.MemoryMap() for region in d._region_list: if region.name == "kernel": @@ -178,6 +179,7 @@ def test_distance_three_regions_bottom_middle_collision(): def test_distance_five_regions_bottom_top_collision(): """ """ + diagram_height = 1000 with unittest.mock.patch('sys.argv', ['mmap_digram.diagram', 'kernel', @@ -198,8 +200,7 @@ def test_distance_five_regions_bottom_top_collision(): "-o", f"/tmp/pytest/{__name__}.md", "-l", - "1000"], - mm.diagram.MemoryMap.height, 1000): + str(diagram_height)]): d = mm.diagram.MemoryMap() for region in d._region_list: if region.name == "kernel": diff --git a/tests/test_docs.py b/tests/test_docs.py index 5b95119..90f3a9c 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -21,8 +21,8 @@ def test_generate_doc_example(): 'uboot-scr', '0x110', '0x30', - "-o", - "doc/example/report.md"], + "-o", "doc/example/report.md", + "-s", "2"], ): d = mm.diagram.MemoryMap() for region in d._region_list: diff --git a/tests/test_scaling.py b/tests/test_scaling.py new file mode 100644 index 0000000..50f43c5 --- /dev/null +++ b/tests/test_scaling.py @@ -0,0 +1,70 @@ +import mm.diagram +import unittest +import mm.types +import PIL.Image +import pathlib + + +def test_scaling_x1(): + """ """ + default_diagram_width = 400 + requested_diagram_height = 400 + requested_scale = 1 + report_path = pathlib.Path(f"/tmp/pytest/{__name__}.md") + image_path = pathlib.Path(f"/tmp/pytest/{__name__}.png") + report_path.unlink(missing_ok=True) + image_path.unlink(missing_ok=True) + + with unittest.mock.patch('sys.argv', + ['mmap_digram.diagram', + 'kernel', + '0x10', + '0x30', + 'rootfs', + '0x50', + '0x30', + 'dtb', + '0x90', + '0x30', + "-o", + str(report_path), + "-l", + str(requested_diagram_height)]): + + d = mm.diagram.MemoryMap() + outimg = PIL.Image.open(str(image_path)) + assert d.height == requested_diagram_height * requested_scale + assert d.width == default_diagram_width * requested_scale + assert outimg.size == (d.width, d.height) + + +def test_scaling_x2(): + """ """ + default_diagram_width = 400 + requested_diagram_height = 400 + requested_scale = 2 + report_path = pathlib.Path(f"/tmp/pytest/{__name__}.md") + image_path = pathlib.Path(f"/tmp/pytest/{__name__}.png") + report_path.unlink(missing_ok=True) + image_path.unlink(missing_ok=True) + + with unittest.mock.patch('sys.argv', + ['mmap_digram.diagram', + 'kernel', + '0x10', + '0x30', + 'rootfs', + '0x50', + '0x30', + 'dtb', + '0x90', + '0x30', + "-o", str(report_path), + "-l", str(requested_diagram_height), + "-s", str(requested_scale)]): + + d = mm.diagram.MemoryMap() + outimg = PIL.Image.open(str(image_path)) + assert d.height == requested_diagram_height * requested_scale + assert d.width == default_diagram_width * requested_scale + assert outimg.size == (d.width, d.height)