diff --git a/src/bindings/python/fluxacct/accounting/bank_subcommands.py b/src/bindings/python/fluxacct/accounting/bank_subcommands.py index 6781867b2..914ee13c8 100644 --- a/src/bindings/python/fluxacct/accounting/bank_subcommands.py +++ b/src/bindings/python/fluxacct/accounting/bank_subcommands.py @@ -21,63 +21,6 @@ # Helper Functions # # # ############################################################### - - -def print_user_rows(cur, rows, bank): - """Print user information in a table format.""" - user_str = "\nUsers Under Bank {bank_name}:\n\n".format(bank_name=bank) - user_headers = [description[0] for description in cur.description] - # print column names of association_table - for header in user_headers: - user_str += header.ljust(18) - user_str += "\n" - for row in rows: - for col in list(row): - user_str += str(col).ljust(18) - user_str += "\n" - - return user_str - - -def get_bank_rows(cur, rows, bank): - """Print bank information in a table format.""" - bank_str = "" - bank_headers = [description[0] for description in cur.description] - # bank has sub banks, so list them - for header in bank_headers: - bank_str += header.ljust(15) - bank_str += "\n" - for row in rows: - for col in list(row): - bank_str += str(col).ljust(15) - bank_str += "\n" - - return bank_str - - -def print_sub_banks(conn, bank, bank_str, indent=""): - """Traverse the bank table and print all sub banks ansd users.""" - select_stmt = "SELECT bank FROM bank_table WHERE parent_bank=?" - cur = conn.cursor() - cur.execute(select_stmt, (bank,)) - result = cur.fetchall() - - # we've reached a bank with no sub banks - if len(result) == 0: - cur.execute("SELECT username FROM association_table WHERE bank=?", (bank,)) - result = cur.fetchall() - if result: - for row in result: - bank_str += indent + " " + row[0] + "\n" - # else, delete all of its sub banks and continue traversing - else: - for row in result: - bank_str += indent + " " + row[0] + "\n" - bank_str = print_sub_banks(conn, row[0], bank_str, indent + " ") - - return bank_str - - def validate_parent_bank(cur, parent_bank): try: cur.execute("SELECT shares FROM bank_table WHERE bank=?", (parent_bank,)) @@ -134,83 +77,6 @@ def reactivate_bank(conn, cur, bank, parent_bank): conn.commit() -def print_hierarchy(cur, bank, hierarchy_str, indent=""): - # look for all sub banks under this parent bank - select_stmt = "SELECT bank,shares,job_usage FROM bank_table WHERE parent_bank=?" - cur.execute(select_stmt, (bank,)) - sub_banks = cur.fetchall() - - if len(sub_banks) == 0: - # we've reached a bank with no sub banks, so print out every user - # under this bank - cur.execute( - "SELECT username,shares,job_usage,fairshare FROM association_table WHERE bank=?", - (bank,), - ) - users = cur.fetchall() - if users: - for user in users: - hierarchy_str += ( - indent - + " " - + bank.ljust(20) - + str(user[0]).rjust(20 - (len(indent) + 1)) - + str(user[1]).rjust(20) - + str(user[2]).rjust(20) - + str(user[3]).rjust(20) - + "\n" - ) - else: - # continue traversing the hierarchy - for sub_bank in sub_banks: - hierarchy_str += ( - indent - + " " - + str(sub_bank[0]).ljust(20) - + "".rjust(20 - (len(indent) + 1)) # this skips the "Username" column - + str(sub_bank[1]).rjust(20) - + str(sub_bank[2]).rjust(20) - + "\n" - ) - hierarchy_str = print_hierarchy( - cur, sub_bank[0], hierarchy_str, indent + " " - ) - - return hierarchy_str - - -def print_parsable_hierarchy(cur, bank, hierarchy_str, indent=""): - # look for all sub banks under this parent bank - select_stmt = "SELECT bank,shares,job_usage FROM bank_table WHERE parent_bank=?" - cur.execute(select_stmt, (bank,)) - sub_banks = cur.fetchall() - - if len(sub_banks) == 0: - # we've reached a bank with no sub banks, so print out every user - # under this bank - cur.execute( - "SELECT username,shares,job_usage,fairshare FROM association_table WHERE bank=?", - (bank,), - ) - users = cur.fetchall() - if users: - for user in users: - hierarchy_str += ( - f"{indent} {bank}|{user[0]}|{user[1]}|{user[2]}|{user[3]}\n" - ) - else: - # continue traversing the hierarchy - for sub_bank in sub_banks: - hierarchy_str += ( - f"{indent} {str(sub_bank[0])}||{str(sub_bank[1])}|{str(sub_bank[2])}\n" - ) - hierarchy_str = print_parsable_hierarchy( - cur, sub_bank[0], hierarchy_str, indent + " " - ) - - return hierarchy_str - - ############################################################### # # # Subcommand Functions # @@ -270,72 +136,39 @@ def add_bank(conn, bank, shares, parent_bank=""): raise sqlite3.IntegrityError(f"bank {bank} already exists in bank_table") -def view_bank(conn, bank, tree=False, users=False, parsable=False): - cur = conn.cursor() - bank_str = "" +def view_bank(conn, bank, tree=False, users=False, parsable=False, cols=None): + if tree and cols is not None: + # tree format cannot be combined with custom formatting, so raise an Exception + raise ValueError(f"--tree option does not support custom formatting") + if parsable and not tree: + # --parsable can only be called with --tree, so raise an Exception + raise ValueError(f"-P/--parsable can only be passed with -t/--tree") + + # use all column names if none are passed in + cols = cols or fluxacct.accounting.BANK_TABLE + try: - cur.execute("SELECT * FROM bank_table WHERE bank=?", (bank,)) - result = cur.fetchall() - - if result: - bank_str = get_bank_rows(cur, result, bank) - else: - raise ValueError(f"bank {bank} not found in bank_table") - - name = result[0][1] - shares = result[0][4] - usage = result[0][5] - - if parsable is True: - # print out the database hierarchy starting with the bank passed in - hierarchy_str = "Bank|Username|RawShares|RawUsage|Fairshare\n" - hierarchy_str += f"{name}||{str(shares)}|{str(round(usage, 2))}\n" - hierarchy_str = print_parsable_hierarchy(cur, bank, hierarchy_str, "") - return hierarchy_str - if tree is True: - # print out the hierarchy view with the specified bank as the root of the tree - hierarchy_str = ( - "Bank".ljust(20) - + "Username".rjust(20) - + "RawShares".rjust(20) - + "RawUsage".rjust(20) - + "Fairshare".rjust(20) - + "\n" - ) - # add the bank passed in to the hierarchy string - hierarchy_str += ( - name.ljust(20) - + "".rjust(20) - + str(shares).rjust(20) - + str(round(usage, 2)).rjust(20) - + "\n" - ) + cur = conn.cursor() - hierarchy_str = print_hierarchy(cur, name, hierarchy_str, "") - bank_str += "\n" + hierarchy_str - # if users is passed in, print out all potential users under - # the passed in bank - if users is True: - select_stmt = """ - SELECT username,userid,default_bank,shares,job_usage, - fairshare,max_running_jobs,queues FROM association_table - WHERE bank=? - """ - cur.execute( - select_stmt, - (bank,), - ) - result = cur.fetchall() + sql.validate_columns(cols, fluxacct.accounting.BANK_TABLE) + # construct SELECT statement + select_stmt = f"SELECT {', '.join(cols)} FROM bank_table WHERE bank=?" + cur.execute(select_stmt, (bank,)) - if result: - user_str = print_user_rows(cur, result, bank) - bank_str += user_str - else: - bank_str += "\nno users under {bank_name}".format(bank_name=bank) + # initialize BankFormatter object + formatter = fmt.BankFormatter(cur, bank) - return bank_str - except sqlite3.OperationalError as exc: - raise sqlite3.OperationalError(f"an sqlite3.OperationalError occurred: {exc}") + if tree: + if parsable: + return formatter.as_parsable_tree(bank) + return formatter.as_tree() + if users: + return formatter.with_users(bank) + return formatter.as_json() + except sqlite3.Error as err: + raise sqlite3.Error(f"view-bank: an sqlite3.Error occurred: {err}") + except ValueError as exc: + raise ValueError(f"view-bank: {exc}") def delete_bank(conn, bank): diff --git a/src/bindings/python/fluxacct/accounting/formatter.py b/src/bindings/python/fluxacct/accounting/formatter.py index 9c8077d46..a49ac27a9 100644 --- a/src/bindings/python/fluxacct/accounting/formatter.py +++ b/src/bindings/python/fluxacct/accounting/formatter.py @@ -13,7 +13,7 @@ class AccountingFormatter: - def __init__(self, cursor): + def __init__(self, cursor, error_msg="no results found in query"): """ Initialize an AccountingFormatter object with a SQLite cursor. @@ -26,7 +26,7 @@ def __init__(self, cursor): if not self.rows: # the SQL query didn't fetch any results; raise an Exception - raise ValueError("no results found in query") + raise ValueError(error_msg) def get_column_names(self): """ @@ -94,3 +94,195 @@ def as_json(self): json_string = json.dumps(table_data, indent=2) return json_string + + +class BankFormatter(AccountingFormatter): + """ + Subclass of AccountingFormatter that includes additional methods for printing + out banks/sub-banks in a hierarchical format and lists of users under banks. + """ + + def __init__(self, cursor, bank_name): + """ + Initialize a BankFormatter object with a SQLite cursor. + Args: + cursor: a SQLite Cursor object that has the results of a SQL query. + bank_name: the name of the bank. + """ + self.bank_name = bank_name + super().__init__( + cursor, error_msg=f"bank {self.bank_name} not found in bank_table" + ) + + def as_tree(self): + """ + Format the flux-accounting bank hierarchy in tree format. The bank passed + into the query will serve as the root of the tree. + + Returns: + hierarchy: the hierarchy of banks in bank_table with the passed-in bank + as the root of the tree. + """ + + def construct_hierarchy(cur, bank, hierarchy, indent=""): + """ + Recursively traverse bank_table and look for sub banks and associations. Add + them to the string representing the hierarchy of banks and users. + + Args: + cur: the SQLite Cursor object used to execute SQL queries. + bank: the current bank being passed to the SQL query. + hierarchy: the string representing the hierarchy of banks and users. + indent: the level of indent for each level of sub bank or users. + Each traversed level will have one additional space (" ") before the + row. + """ + select_stmt = ( + "SELECT bank,shares,job_usage FROM bank_table WHERE parent_bank=?" + ) + cur.execute(select_stmt, (bank,)) + sub_banks = cur.fetchall() + + if len(sub_banks) == 0: + # reached a bank with no sub banks, so get associations under this bank + cur.execute( + "SELECT username,shares,job_usage,fairshare FROM association_table WHERE bank=?", + (bank,), + ) + users = cur.fetchall() + if users: + for user in users: + hierarchy += ( + indent + + " " + + bank.ljust(20) + + str(user[0]).rjust(20 - (len(indent) + 1)) + + str(user[1]).rjust(20) + + str(user[2]).rjust(20) + + str(user[3]).rjust(20) + + "\n" + ) + else: + # continue traversing the hierarchy + for sub_bank in sub_banks: + hierarchy += ( + indent + + " " + + str(sub_bank[0]).ljust(20) + + "".rjust( + 20 - (len(indent) + 1) + ) # this skips the "Username" column + + str(sub_bank[1]).rjust(20) + + str(sub_bank[2]).rjust(20) + + "\n" + ) + hierarchy = construct_hierarchy( + cur, sub_bank[0], hierarchy, indent + " " + ) + + return hierarchy + + # construct header of the hierarchy + hierarchy = ( + "Bank".ljust(20) + + "Username".rjust(20) + + "RawShares".rjust(20) + + "RawUsage".rjust(20) + + "Fairshare".rjust(20) + + "\n" + ) + # add the bank passed in to the hierarchy string + hierarchy += ( + self.rows[0][1].ljust(20) + + "".rjust(20) + + str(self.rows[0][4]).rjust(20) + + str(round(self.rows[0][5], 2)).rjust(20) + + "\n" + ) + + hierarchy = construct_hierarchy(self.cursor, self.rows[0][1], hierarchy, "") + return hierarchy + + def as_parsable_tree(self, bank): + """ + Format the flux-accounting bank hierarchy in a parsable tree format starting with + the bank passed in serving as the root of the tree. Delimit the items in each row + with a pipe ('|') character. + + Returns: + hierarchy: a string representing the hierarchy of banks in the + flux-accounting DB as a parsable tree. + """ + + def construct_parsable_hierarchy(cur, bank, hierarchy, indent=""): + """ + Recursively traverse bank_table and look for sub banks and users and add + them to a string representing the flux-accounting bank hierarchy.. + + Args: + cur: the SQLite Cursor object used to execute SQL queries. + bank: the current bank being passed to the SQL query. + hierarchy: the string holding the parsable hierarchy tree. + indent: the level of indent for each level of sub bank or associations. + Each traversed level will have one additional space " " before the + row. + """ + select_stmt = ( + "SELECT bank,shares,job_usage FROM bank_table WHERE parent_bank=?" + ) + cur.execute(select_stmt, (bank,)) + sub_banks = cur.fetchall() + + if len(sub_banks) == 0: + # reached a bank with no sub banks, so get associations under this bank + cur.execute( + "SELECT username,shares,job_usage,fairshare FROM association_table WHERE bank=?", + (bank,), + ) + users = cur.fetchall() + if users: + for user in users: + hierarchy += ( + f"{indent} {bank}|{user[0]}|{user[1]}|{user[2]}|{user[3]}\n" + ) + else: + # continue traversing the hierarchy + for sub_bank in sub_banks: + hierarchy += f"{indent} {str(sub_bank[0])}||{str(sub_bank[1])}|{str(sub_bank[2])}\n" + hierarchy = construct_parsable_hierarchy( + cur, sub_bank[0], hierarchy, indent + " " + ) + + return hierarchy + + # construct a hierarchy string starting with the bank passed in + hierarchy = "Bank|Username|RawShares|RawUsage|Fairshare\n" + hierarchy += f"{self.rows[0][1]}||{str(self.rows[0][4])}|{str(round(self.rows[0][5], 2))}\n" + hierarchy = construct_parsable_hierarchy(self.cursor, bank, hierarchy, "") + return hierarchy + + def with_users(self, bank): + """ + Print basic information for all of the users under a given bank in table + format. + + Returns: + info: the information for both the bank and basic information for all + users under that bank. + """ + try: + info = self.as_table() + + select_stmt = """SELECT username,default_bank,shares,job_usage,fairshare + FROM association_table + WHERE bank=?""" + self.cursor.execute( + select_stmt, + (bank,), + ) + + formatter = AccountingFormatter(self.cursor) + info += "\n\n" + formatter.as_table() + return info + except ValueError: + return info + f"\n\nno users under {bank}" diff --git a/src/cmd/flux-account-service.py b/src/cmd/flux-account-service.py index 3c9abae50..c6e458222 100755 --- a/src/cmd/flux-account-service.py +++ b/src/cmd/flux-account-service.py @@ -248,6 +248,7 @@ def view_bank(self, handle, watcher, msg, arg): msg.payload["tree"], msg.payload["users"], msg.payload["parsable"], + msg.payload["fields"].split(",") if msg.payload.get("fields") else None, ) payload = {"view_bank": val} diff --git a/src/cmd/flux-account.py b/src/cmd/flux-account.py index 2127b3d6c..41cd33f5a 100755 --- a/src/cmd/flux-account.py +++ b/src/cmd/flux-account.py @@ -320,6 +320,13 @@ def add_view_bank_arg(subparsers): help="list all sub banks in a parsable format with specified bank as root of tree", metavar="PARSABLE", ) + subparser_view_bank.add_argument( + "--fields", + type=str, + help="list of fields to include in JSON output", + default=None, + metavar="BANK_ID,BANK,ACTIVE,PARENT_BANK,SHARES,JOB_USAGE", + ) def add_delete_bank_arg(subparsers): diff --git a/t/Makefile.am b/t/Makefile.am index 2a50f3163..69f5b7494 100644 --- a/t/Makefile.am +++ b/t/Makefile.am @@ -51,7 +51,8 @@ TESTSCRIPTS = \ python/t1004_queue_cmds.py \ python/t1005_project_cmds.py \ python/t1006_job_archive.py \ - python/t1007_formatter.py + python/t1007_formatter.py \ + python/t1008_banks_output.py dist_check_SCRIPTS = \ $(TESTSCRIPTS) \ diff --git a/t/expected/flux_account/A_bank.expected b/t/expected/flux_account/A_bank.expected index f35c6ffec..2e5e8f229 100644 --- a/t/expected/flux_account/A_bank.expected +++ b/t/expected/flux_account/A_bank.expected @@ -1,9 +1,8 @@ -bank_id bank active parent_bank shares job_usage -2 A 1 root 1 0.0 - -Users Under Bank A: - -username userid default_bank shares job_usage fairshare max_running_jobs queues -user5011 5011 A 1 0.0 0.5 5 -user5012 5012 A 1 0.0 0.5 5 +bank_id | bank | active | parent_bank | shares | job_usage +--------+------+--------+-------------+--------+---------- +2 | A | 1 | root | 1 | 0.0 +username | default_bank | shares | job_usage | fairshare +---------+--------------+--------+-----------+---------- +user5011 | A | 1 | 0.0 | 0.5 +user5012 | A | 1 | 0.0 | 0.5 diff --git a/t/expected/flux_account/D_bank.expected b/t/expected/flux_account/D_bank.expected index f83560751..553321fd2 100644 --- a/t/expected/flux_account/D_bank.expected +++ b/t/expected/flux_account/D_bank.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -5 D 1 root 1 0.0 - Bank Username RawShares RawUsage Fairshare D 1 0.0 E 1 0.0 diff --git a/t/expected/flux_account/E_bank.expected b/t/expected/flux_account/E_bank.expected index e7f29cc9a..f041aea40 100644 --- a/t/expected/flux_account/E_bank.expected +++ b/t/expected/flux_account/E_bank.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -6 E 1 D 1 0.0 - Bank Username RawShares RawUsage Fairshare E 1 0.0 E user5030 1 0.0 0.5 diff --git a/t/expected/flux_account/F_bank_tree.expected b/t/expected/flux_account/F_bank_tree.expected index b6cd50528..e408c80a5 100644 --- a/t/expected/flux_account/F_bank_tree.expected +++ b/t/expected/flux_account/F_bank_tree.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -7 F 1 D 1 0.0 - Bank Username RawShares RawUsage Fairshare F 1 0.0 diff --git a/t/expected/flux_account/F_bank_users.expected b/t/expected/flux_account/F_bank_users.expected index 73ea41096..8b0e27458 100644 --- a/t/expected/flux_account/F_bank_users.expected +++ b/t/expected/flux_account/F_bank_users.expected @@ -1,4 +1,5 @@ -bank_id bank active parent_bank shares job_usage -7 F 1 D 1 0.0 +bank_id | bank | active | parent_bank | shares | job_usage +--------+------+--------+-------------+--------+---------- +7 | F | 1 | D | 1 | 0.0 no users under F diff --git a/t/expected/flux_account/full_hierarchy.expected b/t/expected/flux_account/full_hierarchy.expected index 2c1e5d74e..e3d795a9e 100644 --- a/t/expected/flux_account/full_hierarchy.expected +++ b/t/expected/flux_account/full_hierarchy.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1 0.0 - Bank Username RawShares RawUsage Fairshare root 1 0.0 A 1 0.0 diff --git a/t/expected/flux_account/root_bank.expected b/t/expected/flux_account/root_bank.expected index cb42503f2..cb273a2aa 100644 --- a/t/expected/flux_account/root_bank.expected +++ b/t/expected/flux_account/root_bank.expected @@ -1,3 +1,10 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1 0.0 - +[ + { + "bank_id": 1, + "bank": "root", + "active": 1, + "parent_bank": "", + "shares": 1, + "job_usage": 0.0 + } +] diff --git a/t/expected/pop_db/db_hierarchy_base.expected b/t/expected/pop_db/db_hierarchy_base.expected index c3cfb42d5..66f082a2f 100644 --- a/t/expected/pop_db/db_hierarchy_base.expected +++ b/t/expected/pop_db/db_hierarchy_base.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1 0.0 - Bank Username RawShares RawUsage Fairshare root 1 0.0 A 1 0.0 diff --git a/t/expected/pop_db/db_hierarchy_new_users.expected b/t/expected/pop_db/db_hierarchy_new_users.expected index 3a4c0e68a..9684ad35a 100644 --- a/t/expected/pop_db/db_hierarchy_new_users.expected +++ b/t/expected/pop_db/db_hierarchy_new_users.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1 0.0 - Bank Username RawShares RawUsage Fairshare root 1 0.0 A 1 0.0 diff --git a/t/expected/print_hierarchy/small_no_tie.txt b/t/expected/print_hierarchy/small_no_tie.txt index f7c6165b0..ff8370065 100644 --- a/t/expected/print_hierarchy/small_no_tie.txt +++ b/t/expected/print_hierarchy/small_no_tie.txt @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1000 133.0 - Bank Username RawShares RawUsage Fairshare root 1000 133.0 account1 1000 121.0 diff --git a/t/expected/print_hierarchy/small_tie.txt b/t/expected/print_hierarchy/small_tie.txt index 382c3a213..75ac3223f 100644 --- a/t/expected/print_hierarchy/small_tie.txt +++ b/t/expected/print_hierarchy/small_tie.txt @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1000 133.0 - Bank Username RawShares RawUsage Fairshare root 1000 133.0 account1 1000 120.0 diff --git a/t/expected/print_hierarchy/small_tie_all.txt b/t/expected/print_hierarchy/small_tie_all.txt index 5d40fd21b..55415a297 100644 --- a/t/expected/print_hierarchy/small_tie_all.txt +++ b/t/expected/print_hierarchy/small_tie_all.txt @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1000 1332.0 - Bank Username RawShares RawUsage Fairshare root 1000 1332.0 account1 1000 120.0 diff --git a/t/expected/update_fshare/post_fshare_update.expected b/t/expected/update_fshare/post_fshare_update.expected index 51479e108..a9fdb48b0 100644 --- a/t/expected/update_fshare/post_fshare_update.expected +++ b/t/expected/update_fshare/post_fshare_update.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1000 180.0 - Bank Username RawShares RawUsage Fairshare root 1000 180.0 account1 1000 121.0 diff --git a/t/expected/update_fshare/pre_fshare_update.expected b/t/expected/update_fshare/pre_fshare_update.expected index f7c6165b0..ff8370065 100644 --- a/t/expected/update_fshare/pre_fshare_update.expected +++ b/t/expected/update_fshare/pre_fshare_update.expected @@ -1,6 +1,3 @@ -bank_id bank active parent_bank shares job_usage -1 root 1 1000 133.0 - Bank Username RawShares RawUsage Fairshare root 1000 133.0 account1 1000 121.0 diff --git a/t/python/t1008_banks_output.py b/t/python/t1008_banks_output.py new file mode 100755 index 000000000..313d463ac --- /dev/null +++ b/t/python/t1008_banks_output.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 + +############################################################### +# Copyright 2024 Lawrence Livermore National Security, LLC +# (c.f. AUTHORS, NOTICE.LLNS, COPYING) +# +# This file is part of the Flux resource manager framework. +# For details, see https://github.com/flux-framework. +# +# SPDX-License-Identifier: LGPL-3.0 +############################################################### +import unittest +import os +import sqlite3 +import textwrap + +import fluxacct.accounting +from fluxacct.accounting import create_db as c +from fluxacct.accounting import bank_subcommands as b +from fluxacct.accounting import formatter as fmt + + +class TestAccountingCLI(unittest.TestCase): + @classmethod + def setUpClass(self): + # create test accounting database + c.create_db("test_view_banks.db") + global conn + global cur + + conn = sqlite3.connect("test_view_banks.db") + cur = conn.cursor() + + # add some banks, initialize formatter + def test_AccountingFormatter_with_banks(self): + b.add_bank(conn, bank="root", shares=1) + b.add_bank(conn, bank="A", shares=1, parent_bank="root") + + cur.execute("SELECT * FROM bank_table") + formatter = fmt.AccountingFormatter(cur) + + self.assertIsInstance(formatter, fmt.AccountingFormatter) + + def test_default_columns_bank_table(self): + cur.execute("PRAGMA table_info (bank_table)") + columns = cur.fetchall() + bank_table = [column[1] for column in columns] + + self.assertEqual(fluxacct.accounting.BANK_TABLE, bank_table) + + # test JSON output for listing all banks + def test_list_banks_default(self): + expected = textwrap.dedent( + """\ + [ + { + "bank_id": 1, + "bank": "root", + "active": 1, + "parent_bank": "", + "shares": 1, + "job_usage": 0.0 + }, + { + "bank_id": 2, + "bank": "A", + "active": 1, + "parent_bank": "root", + "shares": 1, + "job_usage": 0.0 + } + ] + """ + ) + test = b.list_banks(conn) + self.assertEqual(expected.strip(), test.strip()) + + # test JSON output with custom columns + def test_list_banks_custom_one(self): + expected = textwrap.dedent( + """\ + [ + { + "bank_id": 1 + }, + { + "bank_id": 2 + } + ] + """ + ) + test = b.list_banks(conn, cols=["bank_id"]) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_custom_two(self): + expected = textwrap.dedent( + """\ + [ + { + "bank_id": 1, + "bank": "root" + }, + { + "bank_id": 2, + "bank": "A" + } + ] + """ + ) + test = b.list_banks(conn, cols=["bank_id", "bank"]) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_custom_three(self): + expected = textwrap.dedent( + """\ + [ + { + "bank_id": 1, + "bank": "root", + "active": 1 + }, + { + "bank_id": 2, + "bank": "A", + "active": 1 + } + ] + """ + ) + test = b.list_banks(conn, cols=["bank_id", "bank", "active"]) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_custom_four(self): + expected = textwrap.dedent( + """\ + [ + { + "bank_id": 1, + "bank": "root", + "active": 1, + "parent_bank": "" + }, + { + "bank_id": 2, + "bank": "A", + "active": 1, + "parent_bank": "root" + } + ] + """ + ) + test = b.list_banks(conn, cols=["bank_id", "bank", "active", "parent_bank"]) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_custom_five(self): + expected = textwrap.dedent( + """\ + [ + { + "bank_id": 1, + "bank": "root", + "active": 1, + "parent_bank": "", + "shares": 1 + }, + { + "bank_id": 2, + "bank": "A", + "active": 1, + "parent_bank": "root", + "shares": 1 + } + ] + """ + ) + test = b.list_banks( + conn, cols=["bank_id", "bank", "active", "parent_bank", "shares"] + ) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_custom_six(self): + expected = textwrap.dedent( + """\ + [ + { + "bank_id": 1, + "bank": "root", + "active": 1, + "parent_bank": "", + "shares": 1, + "job_usage": 0.0 + }, + { + "bank_id": 2, + "bank": "A", + "active": 1, + "parent_bank": "root", + "shares": 1, + "job_usage": 0.0 + } + ] + """ + ) + test = b.list_banks( + conn, + cols=["bank_id", "bank", "active", "parent_bank", "shares", "job_usage"], + ) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_table_default(self): + expected = textwrap.dedent( + """\ + bank_id | bank | active | parent_bank | shares | job_usage + --------+------+--------+-------------+--------+---------- + 1 | root | 1 | | 1 | 0.0 + 2 | A | 1 | root | 1 | 0.0 + """ + ) + test = b.list_banks(conn, table=True) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_table_custom_one(self): + expected = textwrap.dedent( + """\ + bank_id + ------- + 1 + 2 + """ + ) + test = b.list_banks(conn, table=True, cols=["bank_id"]) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_table_custom_two(self): + expected = textwrap.dedent( + """\ + bank_id | bank + --------+----- + 1 | root + 2 | A + """ + ) + test = b.list_banks(conn, table=True, cols=["bank_id", "bank"]) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_table_custom_three(self): + expected = textwrap.dedent( + """\ + bank_id | bank | active + --------+------+------- + 1 | root | 1 + 2 | A | 1 + """ + ) + test = b.list_banks(conn, table=True, cols=["bank_id", "bank", "active"]) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_table_custom_four(self): + expected = textwrap.dedent( + """\ + bank_id | bank | active | parent_bank + --------+------+--------+------------ + 1 | root | 1 | + 2 | A | 1 | root + """ + ) + test = b.list_banks( + conn, table=True, cols=["bank_id", "bank", "active", "parent_bank"] + ) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_table_custom_five(self): + expected = textwrap.dedent( + """\ + bank_id | bank | active | parent_bank | shares + --------+------+--------+-------------+------- + 1 | root | 1 | | 1 + 2 | A | 1 | root | 1 + """ + ) + test = b.list_banks( + conn, + table=True, + cols=["bank_id", "bank", "active", "parent_bank", "shares"], + ) + self.assertEqual(expected.strip(), test.strip()) + + def test_list_banks_table_custom_five(self): + expected = textwrap.dedent( + """\ + bank_id | bank | active | parent_bank | shares | job_usage + --------+------+--------+-------------+--------+---------- + 1 | root | 1 | | 1 | 0.0 + 2 | A | 1 | root | 1 | 0.0 + """ + ) + test = b.list_banks( + conn, + table=True, + cols=["bank_id", "bank", "active", "parent_bank", "shares", "job_usage"], + ) + self.assertEqual(expected.strip(), test.strip()) + + # remove database and log file + @classmethod + def tearDownClass(self): + conn.close() + os.remove("test_view_banks.db") + + +def suite(): + suite = unittest.TestSuite() + + return suite + + +if __name__ == "__main__": + from pycotap import TAPTestRunner + + unittest.main(testRunner=TAPTestRunner()) diff --git a/t/t1023-flux-account-banks.t b/t/t1023-flux-account-banks.t index 088bd4819..eabb6a85a 100755 --- a/t/t1023-flux-account-banks.t +++ b/t/t1023-flux-account-banks.t @@ -146,6 +146,11 @@ test_expect_success 'call list-banks with a bad field' ' grep "invalid fields: foo" error.out ' +test_expect_success 'combining --tree with --fields does not work' ' + test_must_fail flux account view-bank root --tree --fields=bank_id > error.out 2>&1 && + grep "tree option does not support custom formatting" error.out +' + test_expect_success 'remove flux-accounting DB' ' rm $(pwd)/FluxAccountingTest.db ' diff --git a/t/t1036-hierarchy-small-no-tie-db.t b/t/t1036-hierarchy-small-no-tie-db.t index 124a6740a..4e0084b48 100755 --- a/t/t1036-hierarchy-small-no-tie-db.t +++ b/t/t1036-hierarchy-small-no-tie-db.t @@ -54,7 +54,7 @@ test_expect_success 'view database hierarchy' ' ' test_expect_success 'view database hierarchy in a parsable format' ' - flux account view-bank -P root > small_no_tie_parsable.test && + flux account view-bank --tree -P root > small_no_tie_parsable.test && test_cmp ${EXPECTED_FILES}/small_no_tie_parsable.txt small_no_tie_parsable.test ' diff --git a/t/t1037-hierarchy-small-tie-db.t b/t/t1037-hierarchy-small-tie-db.t index 0a2afa1b0..38c88fb2c 100755 --- a/t/t1037-hierarchy-small-tie-db.t +++ b/t/t1037-hierarchy-small-tie-db.t @@ -55,7 +55,7 @@ test_expect_success 'view database hierarchy' ' ' test_expect_success 'view database hierarchy in a parsable format' ' - flux account view-bank -P root > small_tie_parsable.test && + flux account view-bank --tree -P root > small_tie_parsable.test && test_cmp ${EXPECTED_FILES}/small_tie_parsable.txt small_tie_parsable.test ' diff --git a/t/t1038-hierarchy-small-tie-all-db.t b/t/t1038-hierarchy-small-tie-all-db.t index dde9bfc67..c868e11f0 100755 --- a/t/t1038-hierarchy-small-tie-all-db.t +++ b/t/t1038-hierarchy-small-tie-all-db.t @@ -58,7 +58,7 @@ test_expect_success 'view database hierarchy' ' ' test_expect_success 'view database hierarchy in a parsable format' ' - flux account view-bank -P root > small_tie_all_parsable.test && + flux account view-bank --tree -P root > small_tie_all_parsable.test && test_cmp ${EXPECTED_FILES}/small_tie_all_parsable.txt small_tie_all_parsable.test '