From 9798d33512dcdf50d3b88a1223fc4913a2a88eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 21 Jan 2025 13:24:48 +0100 Subject: [PATCH 01/92] Implement network splitting for networks with simplified edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The attributes assigned to edges in simplified networks (potentially) contain multiple values for a single edge. Previously, splitting such networks was not supported as it is not obvious how to deal with such edges. Now, we support splitting simplified networks. An edge that comprises of multiple source-edges before simplification should be partitioned again and its partials distributed over the corresponding bins. Singular edges can be handled as prior. Signed-off-by: Maximilian Löffler --- util-split.R | 106 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/util-split.R b/util-split.R index 31da7ee3..40c43ab8 100644 --- a/util-split.R +++ b/util-split.R @@ -22,7 +22,7 @@ ## Copyright 2021 by Niklas Schneider ## Copyright 2021 by Johannes Hostert ## Copyright 2022 by Jonathan Baumann -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## All Rights Reserved. @@ -524,8 +524,7 @@ split.network.time.based = function(network, time.period = "3 months", bins = NU number.windows = NULL, sliding.window = FALSE, remove.isolates = TRUE) { ## extract date attributes from edges - dates = do.call(base::c, igraph::edge_attr(network, "date")) - dates = get.date.from.unix.timestamp(dates) + dates = igraph::edge_attr(network, "date") ## number of windows given (ignoring time period and bins) if (!is.null(number.windows)) { @@ -545,9 +544,15 @@ split.network.time.based = function(network, time.period = "3 months", bins = NU } else { ## specific bins are given, do not use sliding windows sliding.window = FALSE - ## find bins for dates + ## find bins for given dates bins.date = get.date.from.string(bins) - bins.vector = findInterval(dates, bins.date, all.inside = FALSE) + if (is.list(dates)) { + bins.vector = lapply(dates, function(date) { + findInterval(date, bins.date, all.inside = FALSE) + }) + } else { + bins.vector = findInterval(dates, bins.date, all.inside = FALSE) + } bins = seq_len(length(bins.date) - 1) # the last item just closes the last bin } @@ -877,19 +882,75 @@ split.dataframe.by.bins = function(df, bins) { #' @return a list of networks, with the length of 'unique(bins.vector)' split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, remove.isolates = TRUE) { logging::logdebug("split.network.by.bins: starting.") + + ## initialize variables + network.vertices = igraph::subgraph_from_edges(network, c(), delete.vertices = FALSE) + network.edges = igraph::as_data_frame(network, "edges") + edge.attr.names = igraph::edge_attr_names(network) + edge.count = igraph::ecount(network) + ## create a network for each bin of edges nets = parallel::mclapply(bins, function(bin) { logging::logdebug("Splitting network: bin %s", bin) - ## identify edges in the current bin - edges = igraph::E(network)[ bins.vector == bin ] + + ## empty edge data + subnet.edges = list( + vertices = c(), + attributes = c() + ) + + ## collect (partial-)edges in the current bin + for (i in seq_len(edge.count)) { + edge = network.edges[i, ] + + ## edge is singular + if (length(bins.vector[[i]]) == 1) { + + ## if edge belongs to the current bin + if (bins.vector[[i]] == bin) { + subnet.edges[["vertices"]] = c(subnet.edges[["vertices"]], edge[["from"]], edge[["to"]]) + subnet.edges[["attributes"]] = rbind(subnet.edges[["attributes"]], edge[edge.attr.names]) + } + } + + ## edge contains multiple partials + else { + + ## extract partials of the edge that belong to the current bin + which = bins.vector[[i]] == bin + partial.edge = edge + + ## extract all edge attributes and build new edge + for (attr in edge.attr.names) { + attr.values = partial.edge[[attr]] + + ## if attribute is a list, extract only the values that belong to the current bin + ## else, the attribute is a single value and does not need to be adjusted + if (is.list(attr.values)) { + partial.edge[[attr]][[1]] = attr.values[[1]][which] + } + } + + ## add edge to subnet + subnet.edges[["vertices"]] = c(subnet.edges[["vertices"]], partial.edge[["from"]], partial.edge[["to"]]) + subnet.edges[["attributes"]] = rbind(subnet.edges[["attributes"]], partial.edge[edge.attr.names]) + } + } + ## create network based on the current set of edges - g = igraph::subgraph_from_edges(network, edges, delete.vertices = remove.isolates) - return(g) + subnet = igraph::add_edges(network.vertices, subnet.edges[["vertices"]], attr = subnet.edges[["attributes"]]) + if (remove.isolates) { + subnet = igraph::delete_vertices(subnet, which(igraph::degree(subnet) == 0)) + } + + return(subnet) }) + ## set 'bins' attribute, if specified if (!is.null(bins.date)) { attr(nets, "bins") = get.date.from.string(bins.date) } + logging::logdebug("split.network.by.bins: finished.") return(nets) } @@ -1180,21 +1241,36 @@ split.unify.range.names = function(ranges) { split.get.bins.time.based = function(dates, time.period, number.windows = NULL) { logging::logdebug("split.get.bins.time.based: starting.") + ## flatten dates + flat.dates = get.date.from.unix.timestamp(unlist(dates)) + ## generate date bins from given dates if (is.null(number.windows)) { - dates.breaks = generate.date.sequence(min(dates), max(dates), time.period) + dates.breaks = generate.date.sequence(min(flat.dates), max(flat.dates), time.period) } else { - dates.breaks = generate.date.sequence(min(dates), max(dates), length.out = number.windows) + dates.breaks = generate.date.sequence(min(flat.dates), max(flat.dates), length.out = number.windows) } ## as the last bin bound is exclusive, we need to add a second to it - dates.breaks[length(dates.breaks)] = max(dates) + 1 + dates.breaks[length(dates.breaks)] = max(flat.dates) + 1 ## generate charater strings for bins dates.breaks.chr = get.date.string(head(dates.breaks, -1)) ## find bins for given dates - dates.bins = findInterval(dates, dates.breaks, all.inside = FALSE) - ## convert to character factor and set factor's levels appropriately - dates.bins = factor(dates.breaks.chr[dates.bins], levels = dates.breaks.chr) + if (is.list(dates)) { + + ## split each sublist of dates by the dates.breaks + dates.bins = lapply(dates, function(date) { + intervals = findInterval(date, dates.breaks, all.inside = FALSE) + ## convert to character factor and set factor's levels appropriately + factor(dates.breaks.chr[intervals], levels = dates.breaks.chr) + }) + } else { + + ## split dates by the dates.breaks + dates.bins = findInterval(dates, dates.breaks, all.inside = FALSE) + ## convert to character factor and set factor's levels appropriately + dates.bins = factor(dates.breaks.chr[dates.bins], levels = dates.breaks.chr) + } logging::logdebug("split.get.bins.time.based: finished.") From 0ed437c14423c1917f1ba470e7e55db4626d380b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 21 Jan 2025 17:00:23 +0100 Subject: [PATCH 02/92] Optimize splitting simplified edges for performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-split.R | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/util-split.R b/util-split.R index 40c43ab8..8dad8e04 100644 --- a/util-split.R +++ b/util-split.R @@ -889,6 +889,19 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r edge.attr.names = igraph::edge_attr_names(network) edge.count = igraph::ecount(network) + ## pre-distribute edges into bins to optimize for performance + edges.per.bins = setNames(lapply(bins, function(bin) list()), bins) + for (i in seq_len(edge.count)) { + + ## get all bins that the edge belongs to + edge.bins = bins.vector[[i]] + + ## for all bins the edge belongs to, add the edge to the corresponding bin + for (bin in intersect(bins, unique(edge.bins))) { + edges.per.bins[[bin]] = c(edges.per.bins[[bin]], i) + } + } + ## create a network for each bin of edges nets = parallel::mclapply(bins, function(bin) { logging::logdebug("Splitting network: bin %s", bin) @@ -899,8 +912,8 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r attributes = c() ) - ## collect (partial-)edges in the current bin - for (i in seq_len(edge.count)) { + ## construct (partial-)edges in the current bin + for (i in edges.per.bins[[bin]]) { edge = network.edges[i, ] ## edge is singular From 67a6651b94d50cb7c2ab4a74888b0556d607b102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 24 Jan 2025 18:21:22 +0100 Subject: [PATCH 03/92] Remove numeric edge attributes from edges when splitting when necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When separating simplified edges during network splitting, it is not possible to determine which portion of the a numeric edge attribute belongs to which partial edge. Therefore, they lose their semantics in this case and should be removed. Signed-off-by: Maximilian Löffler --- util-split.R | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/util-split.R b/util-split.R index 8dad8e04..ebe45d09 100644 --- a/util-split.R +++ b/util-split.R @@ -902,6 +902,13 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r } } + ## Track whether to remove numeric edge attributes after splitting. + ## Numeric attributes cannot be correctly separated when they + ## appear in multi-partial edges removing their semantic meaning + numeric.attrs = edge.attr.names[EDGE.ATTR.HANDLING[edge.attr.names] == "sum" & edge.attr.names != "weight"] + ignore.numeric.attrs = length(numeric.attrs) == 0 + ignore.weight.attr = !("weight" %in% edge.attr.names) + ## create a network for each bin of edges nets = parallel::mclapply(bins, function(bin) { logging::logdebug("Splitting network: bin %s", bin) @@ -911,19 +918,17 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r vertices = c(), attributes = c() ) + subnet.vertices = network.vertices ## construct (partial-)edges in the current bin for (i in edges.per.bins[[bin]]) { edge = network.edges[i, ] - ## edge is singular - if (length(bins.vector[[i]]) == 1) { + ## edge belongs completely to the current bin + if (all(bins.vector[[i]] == bin)) { - ## if edge belongs to the current bin - if (bins.vector[[i]] == bin) { - subnet.edges[["vertices"]] = c(subnet.edges[["vertices"]], edge[["from"]], edge[["to"]]) - subnet.edges[["attributes"]] = rbind(subnet.edges[["attributes"]], edge[edge.attr.names]) - } + subnet.edges[["vertices"]] = c(subnet.edges[["vertices"]], edge[["from"]], edge[["to"]]) + subnet.edges[["attributes"]] = rbind(subnet.edges[["attributes"]], edge[edge.attr.names]) } ## edge contains multiple partials @@ -933,6 +938,18 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r which = bins.vector[[i]] == bin partial.edge = edge + ## Numeric attributes cannot be correctly separated when they + ## appear in multi-partial edges removing their semantic meaning. + if (!ignore.numeric.attrs) { + ignore.numeric.attrs = TRUE + } + + ## If the weight attribute is not equal to the number of partials, + ## then the weight attribute cannot be meaningfully separated + if (!ignore.weight.attr && partial.edge["weight"] != length(bins.vector[[i]])) { + ignore.weight.attr = TRUE + } + ## extract all edge attributes and build new edge for (attr in edge.attr.names) { attr.values = partial.edge[[attr]] @@ -942,6 +959,11 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r if (is.list(attr.values)) { partial.edge[[attr]][[1]] = attr.values[[1]][which] } + + ## assume equal distribution of weights across partials + else if (attr == "weight") { + partial.edge[[attr]] = sum(which) + } } ## add edge to subnet @@ -950,8 +972,20 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r } } + ## remove numeric attributes if necessary + if (ignore.numeric.attrs && length(numeric.attrs) > 0) { + subnet.edges[["attributes"]] = subnet.edges[["attributes"]][ !colnames(subnet.edges[["attributes"]]) %in% numeric.attrs ] + subnet.vertices = igraph::delete_edge_attr(subnet.vertices, numeric.attrs) + } + + ## remove weight attribute if necessary + if (ignore.weight.attr && "weight" %in% edge.attr.names) { + subnet.edges[["attributes"]] = subnet.edges[["attributes"]][ colnames(subnet.edges[["attributes"]]) != "weight" ] + subnet.vertices = igraph::delete_edge_attr(subnet.vertices, "weight") + } + ## create network based on the current set of edges - subnet = igraph::add_edges(network.vertices, subnet.edges[["vertices"]], attr = subnet.edges[["attributes"]]) + subnet = igraph::add_edges(subnet.vertices, subnet.edges[["vertices"]], attr = subnet.edges[["attributes"]]) if (remove.isolates) { subnet = igraph::delete_vertices(subnet, which(igraph::degree(subnet) == 0)) } From 98ef83158204be2a67b115cb25df5ba375cccf60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 24 Jan 2025 18:22:07 +0100 Subject: [PATCH 04/92] Adjust tests to support splitting of simplified edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-split-network-time-based.R | 158 +++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 6 deletions(-) diff --git a/tests/test-split-network-time-based.R b/tests/test-split-network-time-based.R index b8b10279..4b0a0f04 100644 --- a/tests/test-split-network-time-based.R +++ b/tests/test-split-network-time-based.R @@ -16,7 +16,7 @@ ## Copyright 2020 by Thomas Bock ## Copyright 2018 by Jakob Kronawitter ## Copyright 2022 by Jonathan Baumann -## Copyright 2024 by Maximilian Löffler +## Copyright 2024-2025 by Maximilian Löffler ## All Rights Reserved. context("Splitting functionality, time-based splitting of networks.") @@ -37,6 +37,81 @@ if (!dir.exists(CF.DATA)) CF.DATA = file.path(".", "tests", "codeface-data") ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / ## Split network ----------------------------------------------------------- +## * helper functions ------------------------------------------------------ + +#' Construct a (sub-)network with the same vertices and new edges +#' +#' Note: This method is an adoption of \code{split.network.by.bins} with less functionality. +#' +#' @param network The network to extract the vertices from +#' @param edges The edges to add to the network [default: NULL] +#' @param remove.isolates Whether to remove isolated vertices [default: TRUE] +#' +#' @return The partial edge +#' +#' @seealso \code{split.network.by.bins} +subnet.with.new.edges = function(network, edges = NULL, remove.isolates = TRUE) { + + ## return empty graph if no edges + if (is.null(edges)) { + return(igraph::subgraph_from_edges(network, c(), delete.vertices = remove.isolates)) + } + + ## ensure that edges are in the correct format + if (!is.data.frame(edges)) { + edges = as.data.frame(edges) + } + + ## extract vertices from network and bring edges in correct format + vertices = igraph::subgraph_from_edges(network, c(), delete.vertices = FALSE) + edge.vertices = as.vector(rbind(edges[["from"]], edges[["to"]])) + edge.attributes = edges[igraph::edge_attr_names(network)] + + ## construct subnet + subnet = igraph::add_edges(vertices, edge.vertices, attr = edge.attributes) + + ## remove isolates if requested + if (remove.isolates) { + subnet = igraph::delete_vertices(subnet, which(igraph::degree(subnet) == 0)) + } + + return(subnet) +} + +#' Extract a partial edge from a simplified edge with multiple components +#' +#' Note: This method is an adoption of \code{split.network.by.bins} with less functionality. +#' +#' @param edge The edge to extract the partial edge from +#' @param partial The index of the partial edge to extract +#' +#' @return The partial edge +#' +#' @seealso \code{split.network.by.bins} +extract.partial.edge = function(edge, partial) { + + ## start with a copy of the edge + partial.edge = edge + + ## extract all edge attributes and build new edge + for (attr in names(edge)) { + attr.values = partial.edge[[attr]] + + ## if attribute is a list, extract only the values that belong to the current bin + ## else, the attribute is a single value and does not need to be adjusted + if (is.list(attr.values)) { + partial.edge[[attr]][[1]] = attr.values[[1]][partial] + } + + ## assume equal distribution of weights across partials + else if (attr == "weight") { + partial.edge[[attr]] = 1 + } + } + + return(partial.edge) +} + ## * time-based ------------------------------------------------------------ ## * * time period --------------------------------------------------------- @@ -100,7 +175,24 @@ patrick::with_parameters_test_that("Split a network time-based (time.period = .. ## retrieve author network author.net = net.builder$get.author.network() - expect_error(split.network.time.based(author.net, bins = bins), info = "Illegal split.") + edges = igraph::as_data_frame(author.net, "edges") + expected = list( + "2016-07-12 15:58:59-2016-07-12 16:00:59" = subnet.with.new.edges(author.net, edges[1, ]), + "2016-07-12 16:00:59-2016-07-12 16:02:59" = subnet.with.new.edges(author.net), + "2016-07-12 16:02:59-2016-07-12 16:04:59" = subnet.with.new.edges(author.net), + "2016-07-12 16:04:59-2016-07-12 16:06:33" = subnet.with.new.edges(author.net, rbind(edges[2, ], + edges[3, ], + edges[4, ])) + ) + results = split.network.time.based(author.net, time.period = "2 mins") + + expect_equal(expected.bins, attr(results, "bins")) + + ## check networks + check.identical = mapply(results, expected, FUN = function(r, e) { + igraph::identical_graphs(r, e) + }) + expect_true(all(check.identical), info = "Network equality.") }, patrick::cases( "pasta, synchronicity: FALSE" = list(test.pasta = FALSE, test.synchronicity = FALSE), @@ -227,7 +319,28 @@ patrick::with_parameters_test_that("Split a network time-based (time.period = .. ## retrieve author network author.net = net.builder$get.author.network() - expect_error(split.network.time.based(author.net, bins = bins, sliding.window = TRUE), info = "Illegal split.") + edges = igraph::as_data_frame(author.net, "edges") + expected = list( + "2016-07-12 15:58:59-2016-07-12 16:00:59" = subnet.with.new.edges(author.net, edges[1, ]), + "2016-07-12 15:59:59-2016-07-12 16:01:59" = subnet.with.new.edges(author.net, extract.partial.edge(edges[1, ], 2)), + "2016-07-12 16:00:59-2016-07-12 16:02:59" = subnet.with.new.edges(author.net), + "2016-07-12 16:01:59-2016-07-12 16:03:59" = subnet.with.new.edges(author.net), + "2016-07-12 16:02:59-2016-07-12 16:04:59" = subnet.with.new.edges(author.net), + "2016-07-12 16:03:59-2016-07-12 16:05:59" = subnet.with.new.edges(author.net, rbind(extract.partial.edge(edges[2, ], 1), + extract.partial.edge(edges[3, ], 1))), + "2016-07-12 16:04:59-2016-07-12 16:06:33" = subnet.with.new.edges(author.net, rbind(edges[2, ], + edges[3, ], + edges[4, ])) + ) + results = split.network.time.based(author.net, time.period = "2 mins", sliding.window = TRUE) + + expect_equal(expected.bins, attr(results, "bins")) + + ## check networks + check.identical = mapply(results, expected, FUN = function(r, e) { + igraph::identical_graphs(r, e) + }) + expect_true(all(check.identical), info = "Network equality.") }, patrick::cases( "pasta, synchronicity: FALSE" = list(test.pasta = FALSE, test.synchronicity = FALSE), @@ -297,8 +410,24 @@ patrick::with_parameters_test_that("Split a network time-based (bins = ...), ", ## retrieve author network author.net = net.builder$get.author.network() - expect_error(split.network.time.based(author.net, bins = bins, sliding.window = test.sliding.window), - info = "Illegal split.") + edges = igraph::as_data_frame(author.net, "edges") + expected = list( + "2016-07-12 15:58:00-2016-07-12 16:00:59" = subnet.with.new.edges(author.net, edges[1, ]), + "2016-07-12 16:00:59-2016-07-12 16:02:59" = subnet.with.new.edges(author.net), + "2016-07-12 16:02:59-2016-07-12 16:04:59" = subnet.with.new.edges(author.net), + "2016-07-12 16:04:59-2016-07-12 17:21:43" = subnet.with.new.edges(author.net, rbind(edges[2, ], + edges[3, ], + edges[4, ])) + ) + results = split.network.time.based(author.net, bins = bins, sliding.window = test.sliding.window) + + expect_equal(expected.bins, attr(results, "bins")) + + ## check networks + check.identical = mapply(results, expected, FUN = function(r, e) { + igraph::identical_graphs(r, e) + }) + expect_true(all(check.identical), info = "Network equality.") }, cases.cross.product( patrick::cases( @@ -410,7 +539,24 @@ patrick::with_parameters_test_that("Split a network time-based with equal-sized ## retrieve author network author.net = net.builder$get.author.network() - expect_error(split.network.time.based(author.net, bins = bins), info = "Illegal split.") + edges = igraph::as_data_frame(author.net, "edges") + expected = list( + "2016-07-12 15:58:59-2016-07-12 16:00:53" = subnet.with.new.edges(author.net, edges[1, ]), + "2016-07-12 16:00:53-2016-07-12 16:02:47" = subnet.with.new.edges(author.net), + "2016-07-12 16:02:47-2016-07-12 16:04:41" = subnet.with.new.edges(author.net), + "2016-07-12 16:04:41-2016-07-12 16:06:33" = subnet.with.new.edges(author.net, rbind(edges[2, ], + edges[3, ], + edges[4, ])) + ) + results = split.network.time.based(author.net, number.windows = 4) + + expect_equal(expected.bins, attr(results, "bins")) + + ## check networks + check.identical = mapply(results, expected, FUN = function(r, e) { + igraph::identical_graphs(r, e) + }) + expect_true(all(check.identical), info = "Network equality.") }, patrick::cases( "pasta, synchronicity: FALSE" = list(test.pasta = FALSE, test.synchronicity = FALSE), From 7ec4d83fdeb308a24a350acd808941807b9511f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 27 Jan 2025 16:30:56 +0100 Subject: [PATCH 05/92] Add tests for time-based splitting of simplified networks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-split-network-time-based.R | 156 ++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/tests/test-split-network-time-based.R b/tests/test-split-network-time-based.R index 4b0a0f04..968531ce 100644 --- a/tests/test-split-network-time-based.R +++ b/tests/test-split-network-time-based.R @@ -627,3 +627,159 @@ patrick::with_parameters_test_that("Split a list of networks time-based with equ "pasta, synchronicity: TRUE" = list(test.pasta = TRUE, test.synchronicity = TRUE) ) )) + + +## * * simplified networks ---------------------------------------------------- + +test_that("Split network with singular and simplified edges", { + + ## construct network + edges = data.frame(comb.1. = c("A", "A", "A", "A", "C"), + comb.2. = c("B", "B", "D", "D", "D"), + date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:00:00", + "2025-01-01 12:00:00", "2025-01-01 12:03:00", + "2025-01-01 12:03:00")), + thread = c("", "", "", "", "")) + network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D")) + network = simplify.network(convert.edge.attributes.to.list(network)) + splits = split.network.time.based(network, time.period = "1 mins") + + ## construct expected networks + edges = igraph::as_data_frame(network, "edges") + expected = list( + "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, rbind(edges[1, ], + extract.partial.edge(edges[2, ], 1))), + "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network), + "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), + edges[3, ])) + ) + + ## check networks + check.identical = mapply(splits, expected, FUN = function(s, e) { + igraph::identical_graphs(s, e) + }) + expect_true(all(check.identical), info = "Network equality.") + +}) + +test_that("Split network with numeric edge attributes", { + + ## construct network + edges = data.frame(comb.1. = c("A", "A", "A", "A", "C", "E"), + comb.2. = c("B", "B", "D", "D", "D", "D"), + date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:00:00", + "2025-01-01 12:01:00", "2025-01-01 12:03:00", + "2025-01-01 12:00:00", "2025-01-01 12:03:00")), + thread = c("", "", "", "", "", ""), + diff.size = c(0, 12, 777, 1337, 42, 91)) + network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D", "E")) + network = simplify.network(convert.edge.attributes.to.list(network)) + splits = split.network.time.based(network, time.period = "1 mins") + + ## construct expected networks + edges = igraph::as_data_frame(network, "edges") + expected = list( + "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, rbind(edges[1, ], + edges[3, ])), + "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, extract.partial.edge(edges[2, ], 1)), + "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), + edges[4, ])) + ) + ## remove the 'diff.size' attribute from subnets 2 and 3 as + ## they contain splits of previously simplifed edges + expected[[2]] = igraph::delete_edge_attr(expected[[2]], "diff.size") + expected[[3]] = igraph::delete_edge_attr(expected[[3]], "diff.size") + + ## check networks + check.identical = mapply(splits, expected, FUN = function(s, e) { + igraph::identical_graphs(s, e) + }) + expect_true(all(check.identical), info = "Network equality.") + +}) + +test_that("Split network with arbitrarily set weight attribute (basic)", { + + ## construct network + edges = data.frame(comb.1. = c("A", "A", "A", "A", "C"), + comb.2. = c("B", "B", "D", "D", "D"), + date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:01:00", + "2025-01-01 12:01:00", "2025-01-01 12:03:00", + "2025-01-01 12:03:00")), + thread = c("", "", "", "", ""), + weight = c(3, 4, 1, 1, 3)) + network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D")) + network = simplify.network(convert.edge.attributes.to.list(network)) + splits = split.network.time.based(network, time.period = "1 mins") + + ## construct expected networks + edges = igraph::as_data_frame(network, "edges") + expected = list( + "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, extract.partial.edge(edges[1, ], 1)), + "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[1, ], 2), + extract.partial.edge(edges[2, ], 1))), + "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), + edges[3, ])) + ) + + ## Remove weight from subnet 1 and 2 as both include one partial of A -- B + ## which has a non-standard weight. + ## Do not remove weight from subnet 3 as it only contains a partial from A -- D + ## which is correctly weighted and the complete edge C -- D. + expected[[1]] = igraph::delete_edge_attr(expected[[1]], "weight") + expected[[2]] = igraph::delete_edge_attr(expected[[2]], "weight") + + ## check networks + check.identical = mapply(splits, expected, FUN = function(s, e) { + igraph::identical_graphs(s, e) + }) + expect_true(all(check.identical), info = "Network equality.") + +}) + +test_that("Split network with arbitrarily set weight attribute (complex)", { + + ## construct network + edges = data.frame(comb.1. = c("A", "A", "A", "A", "C", "E"), + comb.2. = c("B", "B", "D", "D", "D", "D"), + date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:00:00", + "2025-01-01 12:01:00", "2025-01-01 12:03:00", + "2025-01-01 12:00:00", "2025-01-01 12:03:00")), + thread = c("", "", "", "", "", ""), + diff.size = c(0, 12, 777, 1337, 42, 91), + weight = c(1, 2, 0, 2, 1, 2)) + network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D", "E")) + network = simplify.network(convert.edge.attributes.to.list(network)) + splits = split.network.time.based(network, time.period = "1 mins") + + ## construct expected networks + edges = igraph::as_data_frame(network, "edges") + expected = list( + "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, rbind(edges[1, ], + edges[3, ])), + "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, extract.partial.edge(edges[2, ], 1)), + "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), + edges[4, ])) + ) + + ## Verify that numeric attributes can be removed independently of + ## the weight attribue: + ## remove the 'diff.size' attribute from subnets 2 and 3 as + ## they contain splits of previously simplifed edges + expected[[2]] = igraph::delete_edge_attr(expected[[2]], "diff.size") + expected[[3]] = igraph::delete_edge_attr(expected[[3]], "diff.size") + + ## Do not remove weight from first subnet as it only contains complete edges + ## even though the weight attribute is non-standard. + ## Do not remove weight from second subnet as the implementation has to assume + ## that A -- D has default weights (even though that is not the case) and + ## therefore split them equally. + ## Remove weight from third subnet as it contains splits of previously simplified edges. + + ## check networks + check.identical = mapply(splits, expected, FUN = function(s, e) { + igraph::identical_graphs(s, e) + }) + expect_true(all(check.identical), info = "Network equality.") + +}) From 637d62ab70f098f26f241e588a99cdc49d10f56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 31 Jan 2025 21:41:13 +0100 Subject: [PATCH 06/92] Change simplification strategy for 'relation' edge attribute to 'concat' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeping all values of the 'relation' edge attribute when simplifying enables a proper separation of the attribute when splitting simplified networks. This change is not in contradiction with #251 (in which the previous simplification strategy was proposed). Signed-off-by: Maximilian Löffler --- util-networks.R | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/util-networks.R b/util-networks.R index 73d7ab6a..6ef94627 100644 --- a/util-networks.R +++ b/util-networks.R @@ -59,7 +59,6 @@ EDGE.ATTR.HANDLING = list( ## network-analytic data weight = "sum", type = "first", - relation = function(relation) sort(unique(relation)), ## commit data changed.files = "sum", @@ -937,7 +936,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## set edge attributes on all edges igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = relation + igraph::E(network)$relation = list(relation) return(network) }) @@ -1003,7 +1002,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## set edge attributes on all edges igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = relation + igraph::E(network)$relation = list(relation) ## set vertex attribute 'kind' on all edges, corresponding to relation vertex.kind = private$get.vertex.kind.for.relation(relation) @@ -1049,7 +1048,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## set edge attributes on all edges igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = relation + igraph::E(network)$relation = list(relation) return(network) }) @@ -1842,7 +1841,7 @@ create.empty.network = function(directed = TRUE, add.attributes = FALSE) { if (add.attributes) { mandatory.edge.attributes.classes = list( date = "list", artifact.type = "list", weight = "numeric", - type = "character", relation = "character" + type = "character", relation = "list" ) mandatory.vertex.attributes.classes = list(name = "character", kind = "character", type = "character") From 2c70666f128f96a3a573f29a0cbbef14d803d193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 31 Jan 2025 21:46:09 +0100 Subject: [PATCH 07/92] Adjust tests to match new 'relation' attribute simplification strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-networks-author.R | 4 ++- tests/test-networks-multi-relation.R | 24 +++++++++------ tests/test-networks.R | 46 ++++++++++++++++------------ 3 files changed, 45 insertions(+), 29 deletions(-) diff --git a/tests/test-networks-author.R b/tests/test-networks-author.R index 43fb347f..3eeec666 100644 --- a/tests/test-networks-author.R +++ b/tests/test-networks-author.R @@ -457,7 +457,8 @@ test_that("Network construction of the undirected simplified author-cochange net list("Base_Feature", "Base_Feature"))), weight = 2, type = TYPE.EDGES.INTRA, - relation = "cochange" + relation = I(list(list("cochange", "cochange"), list("cochange", "cochange"), + list("cochange", "cochange"), list("cochange", "cochange"))) ) ## remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` @@ -466,6 +467,7 @@ test_that("Network construction of the undirected simplified author-cochange net data[["hash"]] = unclass(data[["hash"]]) data[["file"]] = unclass(data[["file"]]) data[["artifact"]] = unclass(data[["artifact"]]) + data[["relation"]] = unclass(data[["relation"]]) ## build expected network network.expected = igraph::graph_from_data_frame(data, directed = FALSE, vertices = authors) diff --git a/tests/test-networks-multi-relation.R b/tests/test-networks-multi-relation.R index d536cade..b7d9e503 100644 --- a/tests/test-networks-multi-relation.R +++ b/tests/test-networks-multi-relation.R @@ -81,8 +81,7 @@ test_that("Network construction of the undirected author network with relation = as.list(rep(NA, 4)))), weight = 1, type = TYPE.EDGES.INTRA, - relation = c(rep("cochange", 8), - rep("mail", 4)), + relation = I(c(as.list(rep("cochange", 8)), as.list(rep("mail", 4)))), message.id = I(c(as.list(rep(NA, 8)), "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", @@ -98,6 +97,7 @@ test_that("Network construction of the undirected author network with relation = data[["hash"]] = unclass(data[["hash"]]) data[["file"]] = unclass(data[["file"]]) data[["artifact"]] = unclass(data[["artifact"]]) + data[["relation"]] = unclass(data[["relation"]]) data[["message.id"]] = unclass(data[["message.id"]]) data[["thread"]] = unclass(data[["thread"]]) @@ -205,7 +205,7 @@ test_that("Construction of the bipartite network for the feature artifact with a as.list(rep(NA, 16)))), weight = 1, type = TYPE.EDGES.INTER, - relation = c(rep("issue", 24), rep("mail", 16)) + relation = I(c(as.list(rep("issue", 24)), as.list(rep("mail", 16)))) ) ## remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` @@ -215,6 +215,7 @@ test_that("Construction of the bipartite network for the feature artifact with a network.expected.data[["thread"]] = unclass(network.expected.data[["thread"]]) network.expected.data[["issue.id"]] = unclass(network.expected.data[["issue.id"]]) network.expected.data[["event.name"]] = unclass(network.expected.data[["event.name"]]) + network.expected.data[["relation"]] = unclass(network.expected.data[["relation"]]) ## 3) build expected network network.expected = igraph::graph_from_data_frame(network.expected.data, vertices = vertices, @@ -309,8 +310,8 @@ test_that("Construction of the multi network for the feature artifact with autho as.list(rep(NA, 21)))), weight = 1, type = c(rep(TYPE.EDGES.INTRA, 13), rep(TYPE.EDGES.INTER, 27)), - relation = c(rep("cochange", 8), rep("mail", 4), rep("cochange", 1), rep("cochange", 6), - rep("issue", 21)), + relation = I(c(as.list(rep("cochange", 8)), as.list(rep("mail", 4)), as.list(rep("cochange", 1)), as.list(rep("cochange", 6)), + as.list(rep("issue", 21)))), message.id = I(c(as.list(rep(NA, 8)), "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", @@ -335,6 +336,7 @@ test_that("Construction of the multi network for the feature artifact with autho edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) + edges[["relation"]] = unclass(edges[["relation"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) edges[["thread"]] = unclass(edges[["thread"]]) edges[["author.name"]] = unclass(edges[["author.name"]]) @@ -425,7 +427,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 24)))), weight = c(rep(1, 30)), type = c(rep("Bipartite", 30)), - relation = c(rep("cochange", 6), rep("issue", 24)), + relation = I(c(as.list(rep("cochange", 6)), as.list(rep("issue", 24)))), issue.id = I(c(as.list(rep(NA, 6)), "", "", "", "", "", "", @@ -444,6 +446,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) + edges[["relation"]] = unclass(edges[["relation"]]) edges[["issue.id"]] = unclass(edges[["issue.id"]]) edges[["event.name"]] = unclass(edges[["event.name"]]) @@ -518,7 +521,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 16)))), weight = rep(1,22), type = rep("Bipartite", 22), - relation = c(rep("cochange", 6), rep("mail", 16)), + relation = I(c(as.list(rep("cochange", 6)), as.list(rep("mail", 16)))), message.id = I(c(as.list(rep(NA, 6)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "", "", @@ -540,6 +543,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) + edges[["relation"]] = unclass(edges[["relation"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) edges[["thread"]] = unclass(edges[["thread"]]) @@ -630,7 +634,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re event.name = I(c(rep("commented", 24), as.list(rep(NA, 16)))), weight = rep(1, 40), type = rep("Bipartite", 40), - relation = c(rep("issue", 24), rep("mail", 16)), + relation = I(c(as.list(rep("issue", 24)), as.list(rep("mail", 16)))), message.id = I(c(as.list(rep(NA, 24)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", @@ -651,6 +655,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re edges[["artifact.type"]] = unclass(edges[["artifact.type"]]) edges[["issue.id"]] = unclass(edges[["issue.id"]]) edges[["event.name"]] = unclass(edges[["event.name"]]) + edges[["relation"]] = unclass(edges[["relation"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) edges[["thread"]] = unclass(edges[["thread"]]) @@ -750,7 +755,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 40)))), weight = rep(1, 46), type = rep("Bipartite", 46), - relation = c(rep("cochange", 6), rep("issue", 24), rep("mail", 16)), + relation = I(c(as.list(rep("cochange", 6)), as.list(rep("issue", 24)), as.list(rep("mail", 16)))), issue.id = I(c(as.list(rep(NA, 6)), "", "", "", "", "", "", "", "", @@ -781,6 +786,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) + edges[["relation"]] = unclass(edges[["relation"]]) edges[["issue.id"]] = unclass(edges[["issue.id"]]) edges[["event.name"]] = unclass(edges[["event.name"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) diff --git a/tests/test-networks.R b/tests/test-networks.R index 1cd093b7..1933aed4 100644 --- a/tests/test-networks.R +++ b/tests/test-networks.R @@ -90,24 +90,28 @@ test_that("Simplify basic multi-relational network", { igraph::make_empty_graph(n = 0, directed = FALSE) + igraph::vertices("A", "B", type = TYPE.ARTIFACT, kind = "feature") for (i in 1:3) { - network = igraph::add_edges(network, c("A", "B"), type = TYPE.EDGES.INTRA, relation = "mail") - network = igraph::add_edges(network, c("A", "B"), type = TYPE.EDGES.INTRA, relation = "cochange") + network = igraph::add_edges(network, c("A", "B"), type = TYPE.EDGES.INTRA, relation = list("mail")) + network = igraph::add_edges(network, c("A", "B"), type = TYPE.EDGES.INTRA, relation = list("cochange")) } network.expected = igraph::make_empty_graph(n = 0, directed = FALSE) + igraph::vertices("A", "B", type = TYPE.ARTIFACT, kind = "feature") + - igraph::edges("A", "B", type = TYPE.EDGES.INTRA, relation = "mail") + - igraph::edges("A", "B", type = TYPE.EDGES.INTRA, relation = "cochange") + igraph::edges("A", "B", "A", "B", type = TYPE.EDGES.INTRA, relation = list(as.list(rep("mail", 3)), + as.list(rep("cochange", 3)))) ## simplify network without simplifying multiple relations into single edges network.simplified = simplify.network(network, simplify.multiple.relations = FALSE) assert.networks.equal(network.simplified, network.expected) + network.expected = igraph::make_empty_graph(n = 0, directed = FALSE) + + igraph::vertices("A", "B", type = TYPE.ARTIFACT, kind = "feature") + + igraph::edges("A", "B", type = TYPE.EDGES.INTRA, relation = list(list("mail", "cochange", + "mail", "cochange", + "mail", "cochange"))) + ## simplify network with simplifying multiple relations into single edges network.simplified = simplify.network(network, simplify.multiple.relations = TRUE) - expect_identical(igraph::ecount(network.simplified), 1) - expect_identical(igraph::E(network.simplified)$type[[1]], "Unipartite") - expect_identical(igraph::E(network.simplified)$relation[[1]], c("cochange", "mail")) + assert.networks.equal(network.simplified, network.expected) }) test_that("Simplify author-network with relation = c('cochange', 'mail') using both algorithms", { @@ -155,7 +159,8 @@ test_that("Simplify author-network with relation = c('cochange', 'mail') using b list("Base_Feature", "Base_Feature"), as.list(rep(NA, 2)), as.list(rep(NA, 2))) data$weight = rep(2, 6) data$type = rep(TYPE.EDGES.INTRA, 6) - data$relation = c(rep("cochange", 4), rep("mail", 2)) + data$relation = list(list("cochange", "cochange"), list("cochange", "cochange"), list("cochange", "cochange"), + list("cochange", "cochange"), list("mail", "mail"), list("mail", "mail")) data$message.id = list(as.list(rep(NA, 2)), as.list(rep(NA, 2)), as.list(rep(NA, 2)), as.list(rep(NA, 2)), list("<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>"), @@ -166,7 +171,7 @@ test_that("Simplify author-network with relation = c('cochange', 'mail') using b ## build expected network network.expected = igraph::graph_from_data_frame(data, vertices = authors, - directed = net.conf$get.value("author.directed")) + directed = net.conf$get.value("author.directed")) ## build simplified network network.built = network.builder$get.author.network() @@ -200,7 +205,8 @@ test_that("Simplify author-network with relation = c('cochange', 'mail') using b list("Base_Feature", "Base_Feature", NA, NA), list("Base_Feature", "Base_Feature")) data$weight = c(4, 2, 4, 2) data$type = rep(TYPE.EDGES.INTRA, 4) - data$relation = list(c("cochange", "mail"), c("cochange"), c("cochange", "mail"), c("cochange")) + data$relation = list(list("cochange", "cochange", "mail", "mail"), list("cochange", "cochange"), + list("cochange", "cochange", "mail", "mail"), list("cochange", "cochange")) data$message.id = list(list(NA, NA, "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>"), list(NA, NA), @@ -238,10 +244,10 @@ test_that("Simplify multiple basic multi-relational networks", { igraph::make_empty_graph(n = 0, directed = FALSE) + igraph::vertices("C", "D", type = TYPE.AUTHOR, kind = TYPE.AUTHOR) for (i in 1:3) { - network.A = igraph::add_edges(network.A, c("A", "B"), type = TYPE.EDGES.INTRA, relation = "mail") - network.A = igraph::add_edges(network.A, c("A", "B"), type = TYPE.EDGES.INTRA, relation = "cochange") - network.B = igraph::add_edges(network.B, c("C", "D"), type = TYPE.EDGES.INTRA, relation = "mail") - network.B = igraph::add_edges(network.B, c("C", "D"), type = TYPE.EDGES.INTRA, relation = "cochange") + network.A = igraph::add_edges(network.A, c("A", "B"), type = TYPE.EDGES.INTRA, relation = list("mail")) + network.A = igraph::add_edges(network.A, c("A", "B"), type = TYPE.EDGES.INTRA, relation = list("cochange")) + network.B = igraph::add_edges(network.B, c("C", "D"), type = TYPE.EDGES.INTRA, relation = list("mail")) + network.B = igraph::add_edges(network.B, c("C", "D"), type = TYPE.EDGES.INTRA, relation = list("cochange")) } ## add graph attributes @@ -251,12 +257,14 @@ test_that("Simplify multiple basic multi-relational networks", { network.A.expected = igraph::make_empty_graph(n = 0, directed = FALSE) + igraph::vertices("A", "B", type = TYPE.ARTIFACT, kind = "feature") + - igraph::edges("A", "B", type = TYPE.EDGES.INTRA, relation = "mail") + - igraph::edges("A", "B", type = TYPE.EDGES.INTRA, relation = "cochange") + igraph::edges("A", "B", "A", "B", type = TYPE.EDGES.INTRA) network.B.expected = igraph::make_empty_graph(n = 0, directed = FALSE) + igraph::vertices("C", "D", type = TYPE.AUTHOR, kind = TYPE.AUTHOR) + - igraph::edges("C", "D", type = TYPE.EDGES.INTRA, relation = "mail") + - igraph::edges("C", "D", type = TYPE.EDGES.INTRA, relation = "cochange") + igraph::edges("C", "D", "C", "D", type = TYPE.EDGES.INTRA) + network.A.expected = igraph::set_edge_attr(network.A.expected, "relation", value = list(list("mail", "mail", "mail"), + list("cochange", "cochange", "cochange"))) + network.B.expected = igraph::set_edge_attr(network.B.expected, "relation", value = list(list("mail", "mail", "mail"), + list("cochange", "cochange", "cochange"))) ## simplify networks without simplifying multiple relations into single edges networks.simplified = simplify.networks(networks, simplify.multiple.relations = FALSE) @@ -272,7 +280,7 @@ test_that("Simplify multiple basic multi-relational networks", { for (i in 1:2) { expect_identical(igraph::ecount(networks.simplified[[i]]), 1) expect_identical(igraph::E(networks.simplified[[i]])$type[[1]], "Unipartite") - expect_identical(igraph::E(networks.simplified[[i]])$relation[[1]], c("cochange", "mail")) + expect_identical(igraph::E(networks.simplified[[i]])$relation[[1]], list("mail", "cochange", "mail", "cochange", "mail", "cochange")) } ## verify graph attributes From 1cbc6fa36859d6db3a7ff4493ef19763e87d2de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 9 Feb 2025 11:34:25 +0100 Subject: [PATCH 08/92] Remove numeric attributes from all splits and keep 'weight' in any case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As discussed in PR#278, removing numeric attributes from some splits while keeping them for others is not benefitial. Hence, we remove them from all splits as soon as one contains non-complete edges. Furthermore, the 'weight' attribute is mandatory and can therefore not be removed. Now, we assume 'weight' to always be equal to the amount of partials that comprise an edge. Signed-off-by: Maximilian Löffler --- util-split.R | 54 +++++++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/util-split.R b/util-split.R index ebe45d09..a7e6c270 100644 --- a/util-split.R +++ b/util-split.R @@ -935,20 +935,12 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r else { ## extract partials of the edge that belong to the current bin - which = bins.vector[[i]] == bin + which.partials = bins.vector[[i]] == bin partial.edge = edge - ## Numeric attributes cannot be correctly separated when they - ## appear in multi-partial edges removing their semantic meaning. - if (!ignore.numeric.attrs) { - ignore.numeric.attrs = TRUE - } - - ## If the weight attribute is not equal to the number of partials, - ## then the weight attribute cannot be meaningfully separated - if (!ignore.weight.attr && partial.edge["weight"] != length(bins.vector[[i]])) { - ignore.weight.attr = TRUE - } + ## numeric attributes cannot be correctly separated when they + ## appear in multi-partial edges removing their semantic meaning + ignore.numeric.attrs = TRUE ## extract all edge attributes and build new edge for (attr in edge.attr.names) { @@ -957,12 +949,12 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r ## if attribute is a list, extract only the values that belong to the current bin ## else, the attribute is a single value and does not need to be adjusted if (is.list(attr.values)) { - partial.edge[[attr]][[1]] = attr.values[[1]][which] + partial.edge[[attr]][[1]] = attr.values[[1]][which.partials] } ## assume equal distribution of weights across partials else if (attr == "weight") { - partial.edge[[attr]] = sum(which) + partial.edge[[attr]] = sum(which.partials) } } @@ -972,27 +964,33 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r } } - ## remove numeric attributes if necessary - if (ignore.numeric.attrs && length(numeric.attrs) > 0) { - subnet.edges[["attributes"]] = subnet.edges[["attributes"]][ !colnames(subnet.edges[["attributes"]]) %in% numeric.attrs ] - subnet.vertices = igraph::delete_edge_attr(subnet.vertices, numeric.attrs) - } - - ## remove weight attribute if necessary - if (ignore.weight.attr && "weight" %in% edge.attr.names) { - subnet.edges[["attributes"]] = subnet.edges[["attributes"]][ colnames(subnet.edges[["attributes"]]) != "weight" ] - subnet.vertices = igraph::delete_edge_attr(subnet.vertices, "weight") - } - - ## create network based on the current set of edges + ## construct sub-network based on the current set of edges subnet = igraph::add_edges(subnet.vertices, subnet.edges[["vertices"]], attr = subnet.edges[["attributes"]]) if (remove.isolates) { subnet = igraph::delete_vertices(subnet, which(igraph::degree(subnet) == 0)) } - return(subnet) + ## return subnet and information about numeric attributes + subnet.information = list( + "subnet" = subnet, + "ignore.numeric.attrs" = ignore.numeric.attrs + ) + + return(subnet.information) }) + ## unpack information + ignore.numeric.attrs = any(sapply(nets, `[[`, "ignore.numeric.attrs")) + nets = lapply(nets, function(net) net[["subnet"]]) + + ## remove numeric attributes if necessary + if (ignore.numeric.attrs && length(numeric.attrs) > 0) { + nets = lapply(nets, function(subnet) { + subnet = igraph::delete_edge_attr(subnet, numeric.attrs) + return(subnet) + }) + } + ## set 'bins' attribute, if specified if (!is.null(bins.date)) { attr(nets, "bins") = get.date.from.string(bins.date) From 41788ff029d038969bfc6b5773e919201c5ac595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 9 Feb 2025 11:43:42 +0100 Subject: [PATCH 09/92] Adjust tests to remove numeric attrs from all splits but 'weight' never --- tests/test-split-network-time-based.R | 117 ++++++++++++-------------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/tests/test-split-network-time-based.R b/tests/test-split-network-time-based.R index 968531ce..94255fce 100644 --- a/tests/test-split-network-time-based.R +++ b/tests/test-split-network-time-based.R @@ -83,12 +83,12 @@ subnet.with.new.edges = function(network, edges = NULL, remove.isolates = TRUE) #' Note: This method is an adoption of \code{split.network.by.bins} with less functionality. #' #' @param edge The edge to extract the partial edge from -#' @param partial The index of the partial edge to extract +#' @param partial The indicess of the partial edge to extract #' #' @return The partial edge #' #' @seealso \code{split.network.by.bins} -extract.partial.edge = function(edge, partial) { +extract.partial.edge = function(edge, partials) { ## start with a copy of the edge partial.edge = edge @@ -100,12 +100,12 @@ extract.partial.edge = function(edge, partial) { ## if attribute is a list, extract only the values that belong to the current bin ## else, the attribute is a single value and does not need to be adjusted if (is.list(attr.values)) { - partial.edge[[attr]][[1]] = attr.values[[1]][partial] + partial.edge[[attr]][[1]] = attr.values[[1]][partials] } ## assume equal distribution of weights across partials else if (attr == "weight") { - partial.edge[[attr]] = 1 + partial.edge[[attr]] = length(partials) } } @@ -634,12 +634,12 @@ patrick::with_parameters_test_that("Split a list of networks time-based with equ test_that("Split network with singular and simplified edges", { ## construct network - edges = data.frame(comb.1. = c("A", "A", "A", "A", "C"), - comb.2. = c("B", "B", "D", "D", "D"), + edges = data.frame(comb.1. = c("A", "A", "A", "A", "A", "C"), + comb.2. = c("B", "B", "D", "D", "D", "D"), date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:00:00", "2025-01-01 12:00:00", "2025-01-01 12:03:00", - "2025-01-01 12:03:00")), - thread = c("", "", "", "", "")) + "2025-01-01 12:03:00", "2025-01-01 12:03:00")), + thread = c("", "", "", "", "", "")) network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D")) network = simplify.network(convert.edge.attributes.to.list(network)) splits = split.network.time.based(network, time.period = "1 mins") @@ -650,7 +650,7 @@ test_that("Split network with singular and simplified edges", { "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, rbind(edges[1, ], extract.partial.edge(edges[2, ], 1))), "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network), - "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), + "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], c(2, 3)), edges[3, ])) ) @@ -664,6 +664,10 @@ test_that("Split network with singular and simplified edges", { test_that("Split network with numeric edge attributes", { + ## + ## splits contain partial edges + ## + ## construct network edges = data.frame(comb.1. = c("A", "A", "A", "A", "C", "E"), comb.2. = c("B", "B", "D", "D", "D", "D"), @@ -685,10 +689,11 @@ test_that("Split network with numeric edge attributes", { "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), edges[4, ])) ) - ## remove the 'diff.size' attribute from subnets 2 and 3 as - ## they contain splits of previously simplifed edges - expected[[2]] = igraph::delete_edge_attr(expected[[2]], "diff.size") - expected[[3]] = igraph::delete_edge_attr(expected[[3]], "diff.size") + ## remove the 'diff.size' attribute as subnet 2 and 3 + ## contain splits of previously simplifed edges + expected = lapply(expected, function(net) { + net = igraph::delete_edge_attr(net, "diff.size") + }) ## check networks check.identical = mapply(splits, expected, FUN = function(s, e) { @@ -696,38 +701,32 @@ test_that("Split network with numeric edge attributes", { }) expect_true(all(check.identical), info = "Network equality.") -}) - -test_that("Split network with arbitrarily set weight attribute (basic)", { + ## + ## splits only contain complete edges + ## ## construct network - edges = data.frame(comb.1. = c("A", "A", "A", "A", "C"), - comb.2. = c("B", "B", "D", "D", "D"), - date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:01:00", - "2025-01-01 12:01:00", "2025-01-01 12:03:00", - "2025-01-01 12:03:00")), - thread = c("", "", "", "", ""), - weight = c(3, 4, 1, 1, 3)) - network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D")) + edges = data.frame(comb.1. = c("A", "A", "A", "A", "C", "E"), + comb.2. = c("B", "B", "D", "D", "D", "D"), + date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:00:00", + "2025-01-01 12:01:00", "2025-01-01 12:01:00", + "2025-01-01 12:00:00", "2025-01-01 12:03:00")), + thread = c("", "", "", "", "", ""), + diff.size = c(0, 12, 777, 1337, 42, 91)) + network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D", "E")) network = simplify.network(convert.edge.attributes.to.list(network)) splits = split.network.time.based(network, time.period = "1 mins") ## construct expected networks edges = igraph::as_data_frame(network, "edges") expected = list( - "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, extract.partial.edge(edges[1, ], 1)), - "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[1, ], 2), - extract.partial.edge(edges[2, ], 1))), - "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), - edges[3, ])) + "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, rbind(edges[1, ], + edges[3, ])), + "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, edges[2, ]), + "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, edges[4, ]) ) - - ## Remove weight from subnet 1 and 2 as both include one partial of A -- B - ## which has a non-standard weight. - ## Do not remove weight from subnet 3 as it only contains a partial from A -- D - ## which is correctly weighted and the complete edge C -- D. - expected[[1]] = igraph::delete_edge_attr(expected[[1]], "weight") - expected[[2]] = igraph::delete_edge_attr(expected[[2]], "weight") + ## do not the remove the 'diff.size' attribute as all subnet + ## only contain complete edges ## check networks check.identical = mapply(splits, expected, FUN = function(s, e) { @@ -737,44 +736,36 @@ test_that("Split network with arbitrarily set weight attribute (basic)", { }) -test_that("Split network with arbitrarily set weight attribute (complex)", { +test_that("Split network with arbitrarily set weight attribute", { ## construct network - edges = data.frame(comb.1. = c("A", "A", "A", "A", "C", "E"), + edges = data.frame(comb.1. = c("A", "A", "A", "A", "A", "C"), comb.2. = c("B", "B", "D", "D", "D", "D"), - date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:00:00", + date = get.date.from.string(c("2025-01-01 12:00:00", "2025-01-01 12:01:00", "2025-01-01 12:01:00", "2025-01-01 12:03:00", - "2025-01-01 12:00:00", "2025-01-01 12:03:00")), - thread = c("", "", "", "", "", ""), - diff.size = c(0, 12, 777, 1337, 42, 91), - weight = c(1, 2, 0, 2, 1, 2)) - network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D", "E")) + "2025-01-01 12:03:00", "2025-01-01 12:03:00")), + thread = c("", "", "", "", "", ""), + weight = c(3, 4, 0, 1, 2, 3)) + network = igraph::graph_from_data_frame(edges, vertices = c("A", "B", "C", "D")) network = simplify.network(convert.edge.attributes.to.list(network)) splits = split.network.time.based(network, time.period = "1 mins") ## construct expected networks edges = igraph::as_data_frame(network, "edges") expected = list( - "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, rbind(edges[1, ], - edges[3, ])), - "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, extract.partial.edge(edges[2, ], 1)), - "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), - edges[4, ])) + "2025-01-01 12:00:00-2025-01-01 12:01:00" = subnet.with.new.edges(network, extract.partial.edge(edges[1, ], 1)), + "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[1, ], 2), + extract.partial.edge(edges[2, ], 1))), + "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], c(2, 3)), + edges[3, ])) ) - - ## Verify that numeric attributes can be removed independently of - ## the weight attribue: - ## remove the 'diff.size' attribute from subnets 2 and 3 as - ## they contain splits of previously simplifed edges - expected[[2]] = igraph::delete_edge_attr(expected[[2]], "diff.size") - expected[[3]] = igraph::delete_edge_attr(expected[[3]], "diff.size") - - ## Do not remove weight from first subnet as it only contains complete edges - ## even though the weight attribute is non-standard. - ## Do not remove weight from second subnet as the implementation has to assume - ## that A -- D has default weights (even though that is not the case) and - ## therefore split them equally. - ## Remove weight from third subnet as it contains splits of previously simplified edges. + ## adjust the weight attribute + ## contains 1 edge comprised of 1 partial + expected[[1]] = igraph::set_edge_attr(expected[[1]], "weight", value = 1) + ## contains 2 edges comprised of 1 partial each + expected[[2]] = igraph::set_edge_attr(expected[[2]], "weight", value = c(1, 1)) + ## contains 1 edge comprised of 2 partials and 1 complete edge with weight 3 + expected[[3]] = igraph::set_edge_attr(expected[[3]], "weight", value = c(2, 3)) ## check networks check.identical = mapply(splits, expected, FUN = function(s, e) { From b042c0dd08e2229514ccd25dee7a119f25b1ab45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 9 Feb 2025 12:14:02 +0100 Subject: [PATCH 10/92] Correctly group multi-relational and regular edges before simplification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When splifying a network without the 'simplify.multiple.relations' options set, we must still consider that input networks may already contain multi-relational edges (as shown in 'showcase.R' when the sample network is simpified again). In our recent meetings we concluded that edges with a 'mail' relation can be simplified together with edges that have a list('mail', 'mail') relation but not with edges that have a list('mail', 'cochange') relation. Further, we must ensure that any multi-relational edges can still be simplified together with multi-relational edges that have the same relation mix. Signed-off-by: Maximilian Löffler --- util-conf.R | 2 +- util-networks.R | 60 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/util-conf.R b/util-conf.R index 85aec34a..eee1f16d 100644 --- a/util-conf.R +++ b/util-conf.R @@ -898,7 +898,7 @@ NetworkConf = R6::R6Class("NetworkConf", inherit = Conf, type = "logical", allowed = c(TRUE, FALSE), allowed.number = 1 - ), + ), skip.threshold = list( default = Inf, type = "numeric", diff --git a/util-networks.R b/util-networks.R index 6ef94627..caf21080 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1946,25 +1946,57 @@ simplify.network = function(network, remove.multiple = TRUE, remove.loops = TRUE ## save network attributes, otherwise they get lost network.attributes = igraph::graph_attr(network) - if (!simplify.multiple.relations && length(unique(igraph::edge_attr(network, "relation"))) > 1) { + if (!simplify.multiple.relations && length(unique(unlist(igraph::edge_attr(network, "relation")))) > 1) { ## data frame of the network edge.data = igraph::as_data_frame(network, what = "edges") vertex.data = igraph::as_data_frame(network, what = "vertices") + ## helper function to check if two edges can be simplified + relations.match = function(relation.A, relation.B) { + diff = setdiff(sort(unique(unlist(relation.A))), sort(unique(unlist(relation.B)))) + return(length(diff) == 0) + } + + ## group all edges that can be simplified together + edges.by.relation = list() + for (i in seq_len(nrow(edge.data))) { + + ## get relation of current edge + edge.relation = edge.data[i, ][["relation"]] + match = NULL + + ## test if current edge can be simplified with any already seen edge + for (group in seq(edges.by.relation)) { + group.edge.index = edges.by.relation[[group]][1] + group.relation = edge.data[group.edge.index, ][["relation"]] + if (relations.match(edge.relation, group.relation)) { + match = group + break + } + } + + ## add edge to existing group or create a new group + if (!is.null(match)) { + edges.by.relation[[match]] = c(edges.by.relation[[match]], i) + } else { + edges.by.relation[[length(edges.by.relation) + 1]] = i + } + } + ## select edges of one relation, build the network and simplify this network - networks = lapply(unique(edge.data[["relation"]]), - function(relation) { - network.data = edge.data[edge.data[["relation"]] == relation, ] - net = igraph::graph_from_data_frame(d = network.data, - vertices = vertex.data, - directed = igraph::is_directed(network)) - - ## simplify networks (contract edges and remove loops) - net = igraph::simplify(net, edge.attr.comb = EDGE.ATTR.HANDLING, - remove.multiple = remove.multiple, - remove.loops = remove.loops) - ## TODO perform simplification on edge list? - return(net) + networks = lapply(seq(edges.by.relation), + function(group) { + network.data = edge.data[edges.by.relation[[group]], ] + net = igraph::graph_from_data_frame(d = network.data, + vertices = vertex.data, + directed = igraph::is_directed(network)) + + ## simplify networks (contract edges and remove loops) + net = igraph::simplify(net, edge.attr.comb = EDGE.ATTR.HANDLING, + remove.multiple = remove.multiple, + remove.loops = remove.loops) + ## TODO perform simplification on edge list? + return(net) }) ## merge the simplified networks From 36d23d657f412aa1953c4773076e593273f19d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 11 Feb 2025 15:29:41 +0100 Subject: [PATCH 11/92] Add test for simplification of complex multi-relational networks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-networks.R | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test-networks.R b/tests/test-networks.R index 1933aed4..61569521 100644 --- a/tests/test-networks.R +++ b/tests/test-networks.R @@ -288,6 +288,55 @@ test_that("Simplify multiple basic multi-relational networks", { expect_identical(igraph::graph_attr(networks.simplified[["B"]], "name"), "network.B") }) +test_that("Simplify network with multi-relational edges", { + + ## Note: Base network + ## A -- (cochange) --> D + ## B -- (cochange) --> E + ## B -- (mail) --> E + ## C -- (mail) --> F + ## B -- (mail, cochange) --> E + ## B -- (mail, cochange, mail) --> E + ## B -- (mail, mail) --> E + ## C -- (mail, mail) --> F + + ## create network with vertices connected by multi-relational edges + data = data.frame(comb.1. = c("A", "B", "B", "C", "B", "B", "B", "C"), + comb.2. = c("D", "E", "E", "F", "E", "E", "E", "F")) + data$relation = list("cochange", "cochange", + "mail", "mail", + list("mail", "cochange"), list("mail", "cochange", "mail"), + list("mail", "mail"), list("mail", "mail")) + + ## build expected network + network.built = igraph::graph_from_data_frame(data, vertices = c("A", "B", "C", "D", "E", "F"), + directed = FALSE) + + ## ---------------------- simplify.multiple.relations == FALSE -------------------------- ## + + network.built = simplify.network(network.built, simplify.multiple.relations = FALSE) + + ## Note: (mail) can be simplified with (mail, mail). + ## (mail) and (mail, mail) cannot be simplified with (mail, cochange). + ## (mail, cochange) can be simplified with (mail, cochange, mail). + ## A -- (cochange) --> D + ## B -- (cochange) --> E + ## B -- (mail, mail, mail) --> E + ## C -- (mail, mail, mail) --> F + ## B -- (mail, cochange, mail, cochange, mail) --> E + + data = data.frame(comb.1. = c("A", "B", "B", "C", "B"), + comb.2. = c("D", "E", "E", "F", "E")) + data$relation = list("cochange", "cochange", + list("mail", "mail", "mail"), list("mail", "mail", "mail"), + list("mail", "cochange", "mail", "cochange", "mail")) + network.expected = igraph::graph_from_data_frame(data, vertices = c("A", "B", "C", "D", "E", "F"), + directed = FALSE) + + assert.networks.equal(network.built, network.expected) + +}) + test_that("Remove isolated vertices", { ## construct network From 402c256d9a05e4ffb297d4ea1fc25d0230787bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 14 Feb 2025 12:12:27 +0100 Subject: [PATCH 12/92] Deploy custom simplification strategy to replace "concat" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previously used "concat" strategy does not work flawlessly with list edge attributes. When simplifying already simplified edges, i.e., edges that have list attributes longer than one, using the "concat" strategy, the result is a top-level list containing sublists from the merged attribute values. Instead, we want a single top-level list that contains all attribute values from the edges that comprise it. Signed-off-by: Maximilian Löffler --- util-networks.R | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/util-networks.R b/util-networks.R index caf21080..223048b9 100644 --- a/util-networks.R +++ b/util-networks.R @@ -68,7 +68,12 @@ EDGE.ATTR.HANDLING = list( artifact.diff.size = "sum", ## everything else - "concat" + function(attr) { + if (any(sapply(attr, is.list))) { + attr = do.call(base::c, attr) + } + return(attr) + } ) From cf8f2fe3f6df1624e295584aab2d8103fe2c5eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 24 Feb 2025 17:42:01 +0100 Subject: [PATCH 13/92] Miscellaneous code consistency fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-networks-author.R | 2 +- tests/test-networks-multi-relation.R | 2 +- tests/test-networks.R | 2 +- tests/test-split-network-time-based.R | 8 ++++---- util-networks.R | 5 ++++- util-split.R | 8 +++++--- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/test-networks-author.R b/tests/test-networks-author.R index 3eeec666..6e7172dc 100644 --- a/tests/test-networks-author.R +++ b/tests/test-networks-author.R @@ -21,7 +21,7 @@ ## Copyright 2018 by Jakob Kronawitter ## Copyright 2018-2019 by Anselm Fehnker ## Copyright 2021 by Johannes Hostert -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. diff --git a/tests/test-networks-multi-relation.R b/tests/test-networks-multi-relation.R index b7d9e503..7de7fb92 100644 --- a/tests/test-networks-multi-relation.R +++ b/tests/test-networks-multi-relation.R @@ -19,7 +19,7 @@ ## Copyright 2019 by Anselm Fehnker ## Copyright 2021 by Johannes Hostert ## Copyright 2022 by Jonathan Baumann -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. diff --git a/tests/test-networks.R b/tests/test-networks.R index 61569521..463ebd5b 100644 --- a/tests/test-networks.R +++ b/tests/test-networks.R @@ -13,7 +13,7 @@ ## ## Copyright 2018-2019 by Claus Hunsen ## Copyright 2021 by Niklas Schneider -## Copyright 2024 by Maximilian Löffler +## Copyright 2024-2025 by Maximilian Löffler ## All Rights Reserved. diff --git a/tests/test-split-network-time-based.R b/tests/test-split-network-time-based.R index 94255fce..9c4653d4 100644 --- a/tests/test-split-network-time-based.R +++ b/tests/test-split-network-time-based.R @@ -83,7 +83,7 @@ subnet.with.new.edges = function(network, edges = NULL, remove.isolates = TRUE) #' Note: This method is an adoption of \code{split.network.by.bins} with less functionality. #' #' @param edge The edge to extract the partial edge from -#' @param partial The indicess of the partial edge to extract +#' @param partials The indicess of the partial edge to extract #' #' @return The partial edge #' @@ -689,7 +689,7 @@ test_that("Split network with numeric edge attributes", { "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, rbind(extract.partial.edge(edges[2, ], 2), edges[4, ])) ) - ## remove the 'diff.size' attribute as subnet 2 and 3 + ## remove the 'diff.size' attribute as subnets 2 and 3 ## contain splits of previously simplifed edges expected = lapply(expected, function(net) { net = igraph::delete_edge_attr(net, "diff.size") @@ -725,8 +725,8 @@ test_that("Split network with numeric edge attributes", { "2025-01-01 12:01:00-2025-01-01 12:02:00" = subnet.with.new.edges(network, edges[2, ]), "2025-12-01 12:02:00-2025-01-01 12:03:01" = subnet.with.new.edges(network, edges[4, ]) ) - ## do not the remove the 'diff.size' attribute as all subnet - ## only contain complete edges + ## do not the remove the 'diff.size' attribute as all subnets + ## contain only complete edges ## check networks check.identical = mapply(splits, expected, FUN = function(s, e) { diff --git a/util-networks.R b/util-networks.R index 223048b9..57e4e899 100644 --- a/util-networks.R +++ b/util-networks.R @@ -67,7 +67,10 @@ EDGE.ATTR.HANDLING = list( diff.size = "sum", artifact.diff.size = "sum", - ## everything else + ## everything else: + ## + ## this helper function concatenates attribute + ## values together into a single list function(attr) { if (any(sapply(attr, is.list))) { attr = do.call(base::c, attr) diff --git a/util-split.R b/util-split.R index a7e6c270..ad5d4ec6 100644 --- a/util-split.R +++ b/util-split.R @@ -1303,15 +1303,17 @@ split.get.bins.time.based = function(dates, time.period, number.windows = NULL) ## find bins for given dates if (is.list(dates)) { - ## split each sublist of dates by the dates.breaks + ## split each sublist of dates by the 'dates.breaks' dates.bins = lapply(dates, function(date) { intervals = findInterval(date, dates.breaks, all.inside = FALSE) ## convert to character factor and set factor's levels appropriately - factor(dates.breaks.chr[intervals], levels = dates.breaks.chr) + factors = factor(dates.breaks.chr[intervals], levels = dates.breaks.chr) + return(factors) }) + } else { - ## split dates by the dates.breaks + ## split dates by the 'dates.breaks' dates.bins = findInterval(dates, dates.breaks, all.inside = FALSE) ## convert to character factor and set factor's levels appropriately dates.bins = factor(dates.breaks.chr[dates.bins], levels = dates.breaks.chr) From 54af2b19a112070f10d191b98b055482748426a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 24 Feb 2025 17:49:01 +0100 Subject: [PATCH 14/92] Keep numeric attributes when possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When splitting multi-partial edges, we only need to remove numeric attributes (from all edges in all bins) when there is at least one edge that has partials that belong to different bins. When for all edges all partials belong to the same bin, we can keep the numeric attributes. Signed-off-by: Maximilian Löffler --- util-split.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/util-split.R b/util-split.R index ad5d4ec6..11bb1ad9 100644 --- a/util-split.R +++ b/util-split.R @@ -906,8 +906,7 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r ## Numeric attributes cannot be correctly separated when they ## appear in multi-partial edges removing their semantic meaning numeric.attrs = edge.attr.names[EDGE.ATTR.HANDLING[edge.attr.names] == "sum" & edge.attr.names != "weight"] - ignore.numeric.attrs = length(numeric.attrs) == 0 - ignore.weight.attr = !("weight" %in% edge.attr.names) + ignore.numeric.attrs = FALSE ## create a network for each bin of edges nets = parallel::mclapply(bins, function(bin) { @@ -940,7 +939,9 @@ split.network.by.bins = function(network, bins, bins.vector, bins.date = NULL, r ## numeric attributes cannot be correctly separated when they ## appear in multi-partial edges removing their semantic meaning - ignore.numeric.attrs = TRUE + if (!all(which.partials)) { + ignore.numeric.attrs = TRUE + } ## extract all edge attributes and build new edge for (attr in edge.attr.names) { From 6fae1843740ed8e48c89c2ee4e61f995b5d0b8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 24 Feb 2025 18:11:05 +0100 Subject: [PATCH 15/92] Ensure edge attribute values are of list type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nomenclature: An "edge attribute" refers to the values of an attribute over all edges of a network, while an "edge attribute value" refers to the value of an edge attribute of a single edge. Previously we decided that edge attributes by default are of list type, yet we did not specify of which type edge attribute values should be. This is especially relevant when an edge attribute value consists of multiple values, i.e., in simplified edges, and the interplay of simplified edges and non-simplified edges when simplifying repeatedly. By default, simplification with the "concat" strategy wraps the values of all source edges into a list. Therefore, we decided that not only edge attributes should be lists, but also edge attribute values. Signed-off-by: Maximilian Löffler --- util-networks.R | 24 ++++++++++++++++++++---- util-split.R | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/util-networks.R b/util-networks.R index 57e4e899..1cf207a6 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1309,6 +1309,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(u, "range") = private$proj.data$get.range() } + u = convert.edge.attributes.to.list(u) return(u) } @@ -1672,9 +1673,9 @@ merge.network.data = function(vertex.data, edge.data) { all.columns = Reduce(union, lapply(edge.data.filtered, colnames)) edge.data.filtered = lapply(edge.data.filtered, function(edges) { missing.columns = setdiff(all.columns, colnames(edges)) - for (column in missing.columns) { - edges[[column]] = NA - } + edges[missing.columns] = lapply(missing.columns, function(column) { + return(list(list(NA))) + }) return(edges) }) ## 3) call rbind @@ -1814,7 +1815,12 @@ add.edges.for.bipartite.relation = function(net, bipartite.relations, network.co edge.attrs = names(extra.edge.attributes) which.attrs = !(edge.attrs %in% names(EDGE.ATTR.HANDLING)) for (attr in edge.attrs[which.attrs]) { - extra.edge.attributes[[attr]] = as.list(extra.edge.attributes[[attr]]) + list.attr = as.list(extra.edge.attributes[[attr]]) + list.values = sapply(list.attr, is.list) + if (!all(list.values)) { + list.attr[!list.values] = lapply(list.attr[!list.values], as.list) + } + extra.edge.attributes[[attr]] = list.attr } ## add the vertex sequences as edges to the network @@ -2004,6 +2010,7 @@ simplify.network = function(network, remove.multiple = TRUE, remove.loops = TRUE remove.multiple = remove.multiple, remove.loops = remove.loops) ## TODO perform simplification on edge list? + net = convert.edge.attributes.to.list(net) return(net) }) @@ -2012,6 +2019,7 @@ simplify.network = function(network, remove.multiple = TRUE, remove.loops = TRUE } else { network = igraph::simplify(network, edge.attr.comb = EDGE.ATTR.HANDLING, remove.multiple = remove.multiple, remove.loops = remove.loops) + network = convert.edge.attributes.to.list(network) } ## re-apply all network attributes @@ -2236,6 +2244,14 @@ convert.edge.attributes.to.list = function(network, remain.as.is = names(EDGE.AT ## convert edge attributes to list type for (attr in edge.attrs[which.attrs]) { list.attr = as.list(igraph::edge_attr(network, attr)) + + ## convert individual values to list + listed.values = sapply(list.attr, is.list) + if (!all(listed.values)) { + list.attr[!listed.values] = lapply(list.attr[!listed.values], as.list) + } + + ## replace attribute network = igraph::set_edge_attr(network, attr, value = list.attr) } diff --git a/util-split.R b/util-split.R index 11bb1ad9..88533431 100644 --- a/util-split.R +++ b/util-split.R @@ -714,7 +714,7 @@ split.network.activity.based = function(network, number.edges = 5000, number.win number.edges, number.windows) ## get dates in a data.frame for splitting purposes - dates = do.call(base::c, igraph::edge_attr(network, "date")) + dates = unlist(igraph::edge_attr(network, "date")) df = data.frame( date = get.date.from.unix.timestamp(dates), my.unique.id = seq_len(edge.count) # as a unique identifier only From 416c817998540fc0b82d9959574838b571b4d6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 18 Feb 2025 17:04:38 +0100 Subject: [PATCH 16/92] Adjust tests to work with edge attribute values of list type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-networks-equal-constructions.R | 43 ++-- tests/test-networks-multi-relation.R | 262 ++++++++++------------ tests/test-networks.R | 35 ++- 3 files changed, 173 insertions(+), 167 deletions(-) diff --git a/tests/test-networks-equal-constructions.R b/tests/test-networks-equal-constructions.R index e64972da..c280285c 100644 --- a/tests/test-networks-equal-constructions.R +++ b/tests/test-networks-equal-constructions.R @@ -15,7 +15,7 @@ ## Copyright 2018 by Claus Hunsen ## Copyright 2020 by Thomas Bock ## Copyright 2022 by Jonathan Baumann -## Copyright 2024 by Maximilian Löffler +## Copyright 2024-2025 by Maximilian Löffler ## All Rights Reserved. @@ -39,27 +39,30 @@ if (!dir.exists(CF.DATA)) CF.DATA = file.path(".", "tests", "codeface-data") #' @param split.networks.two the second list of split networks compare.edge.and.vertex.lists = function(split.networks.one, split.networks.two) { - for (i in seq_along(split.networks.one)) { - edges.one = igraph::as_data_frame(split.networks.one[[i]], what = "edges") - ordering = order(edges.one[["from"]], edges.one[["to"]], - as.vector(edges.one[["date"]], "numeric")) - edges.one = edges.one[ordering, ] - rownames(edges.one) = seq_len(nrow(edges.one)) - edges.two = igraph::as_data_frame(split.networks.two[[i]], what = "edges") - ordering = order(edges.two[["from"]], edges.two[["to"]], - as.vector(edges.two[["date"]], "numeric")) - edges.two = edges.two[ordering, ] - rownames(edges.two) = seq_len(nrow(edges.two)) - vertices.one = igraph::as_data_frame(split.networks.one[[i]], what = "vertices") - ordering = order(vertices.one[["name"]]) - vertices.one = vertices.one[ordering, ] - rownames(vertices.one) = seq_len(nrow(vertices.one)) - vertices.two = igraph::as_data_frame(split.networks.two[[i]], what = "vertices") - ordering = order(vertices.two[["name"]]) - vertices.two = vertices.two[ordering, ] - rownames(vertices.two) = seq_len(nrow(vertices.two)) + ## helper function to order edges + order.edges = function(edges) { + ordering = order(edges[["from"]], edges[["to"]], + sapply(edges[["date"]], function(date) sum(unlist(date), rm.na = TRUE))) + edges = edges[ordering, ] + rownames(edges) = seq_len(nrow(edges)) + return(edges) + } + + # helper function to order vertices + order.vertices = function(vertices) { + ordering = order(vertices[["name"]]) + vertices = vertices[ordering, ] + rownames(vertices) = seq_len(nrow(vertices)) + return(vertices) + } + for (i in seq_along(split.networks.one)) { + edges.one = order.edges(igraph::as_data_frame(split.networks.one[[i]], what = "edges")) + edges.two = order.edges(igraph::as_data_frame(split.networks.two[[i]], what = "edges")) expect_identical(edges.one, edges.two) + + vertices.one = order.vertices(igraph::as_data_frame(split.networks.one[[i]], what = "vertices")) + vertices.two = order.vertices(igraph::as_data_frame(split.networks.two[[i]], what = "vertices")) expect_identical(vertices.one, vertices.two) } } diff --git a/tests/test-networks-multi-relation.R b/tests/test-networks-multi-relation.R index 7de7fb92..7551a9e7 100644 --- a/tests/test-networks-multi-relation.R +++ b/tests/test-networks-multi-relation.R @@ -63,13 +63,13 @@ test_that("Network construction of the undirected author network with relation = "Björn", "Björn", "Olaf", "Olaf"), # mail comb.2. = c("Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", # cochange "Olaf", "Olaf", "Thomas", "Thomas"), # mail - date = I(as.list(get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", # cochange - "2016-07-12 16:06:10", "2016-07-12 16:05:41", "2016-07-12 16:06:32", - "2016-07-12 16:06:10", "2016-07-12 16:06:32", - "2016-07-12 15:58:40", "2016-07-12 15:58:50", "2016-07-12 16:04:40", # mail - "2016-07-12 16:05:37")))), - artifact.type = I(c(as.list(rep("Feature", 8)), # cochange - as.list(rep("Mail", 4)))), # mail + date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", # cochange + "2016-07-12 16:06:10", "2016-07-12 16:05:41", "2016-07-12 16:06:32", + "2016-07-12 16:06:10", "2016-07-12 16:06:32", + "2016-07-12 15:58:40", "2016-07-12 15:58:50", "2016-07-12 16:04:40", # mail + "2016-07-12 16:05:37")), + artifact.type = c(rep("Feature", 8), # cochange + rep("Mail", 4)), # mail hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", @@ -81,7 +81,7 @@ test_that("Network construction of the undirected author network with relation = as.list(rep(NA, 4)))), weight = 1, type = TYPE.EDGES.INTRA, - relation = I(c(as.list(rep("cochange", 8)), as.list(rep("mail", 4)))), + relation = c(rep("cochange", 8), rep("mail", 4)), message.id = I(c(as.list(rep(NA, 8)), "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", @@ -92,18 +92,16 @@ test_that("Network construction of the undirected author network with relation = ) ## remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - data[["date"]] = unclass(data[["date"]]) - data[["artifact.type"]] = unclass(data[["artifact.type"]]) data[["hash"]] = unclass(data[["hash"]]) data[["file"]] = unclass(data[["file"]]) data[["artifact"]] = unclass(data[["artifact"]]) - data[["relation"]] = unclass(data[["relation"]]) data[["message.id"]] = unclass(data[["message.id"]]) data[["thread"]] = unclass(data[["thread"]]) ## build expected network network.expected = igraph::graph_from_data_frame(data, vertices = authors, - directed = net.conf$get.value("author.directed")) + directed = net.conf$get.value("author.directed")) + network.expected = convert.edge.attributes.to.list(network.expected) expect_true(igraph::identical_graphs(network.built, network.expected)) }) @@ -166,21 +164,21 @@ test_that("Construction of the bipartite network for the feature artifact with a "", "", "", "", "", "", "", # mail "", "", "", "", "", "", "", "", ""), - date = I(as.list(get.date.from.string(c("2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", # issue - "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", - "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", - "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2016-07-12 15:59:59", - "2016-07-15 20:07:47", "2016-07-27 20:12:08", "2016-07-28 06:27:52", - "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", - "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", - "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59", - "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", # mail - "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", - "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", - "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", - "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", - "2010-07-12 10:05:36")))), - artifact.type = I(c(as.list(rep("IssueEvent", 24)), as.list(rep("Mail", 16)))), + date = get.date.from.string(c("2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", # issue + "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", + "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", + "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2016-07-12 15:59:59", + "2016-07-15 20:07:47", "2016-07-27 20:12:08", "2016-07-28 06:27:52", + "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", + "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", + "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59", + "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", # mail + "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", + "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", + "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", + "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", + "2010-07-12 10:05:36")), + artifact.type = c(rep("IssueEvent", 24), rep("Mail", 16)), message.id = I(c(as.list(rep(NA, 24)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", @@ -201,25 +199,22 @@ test_that("Construction of the bipartite network for the feature artifact with a "", "", "", "", "", "", "", "", "", as.list(rep(NA,16)))), - event.name = I(c(rep("commented", 24), - as.list(rep(NA, 16)))), + event.name = I(c(rep("commented", 24), as.list(rep(NA, 16)))), weight = 1, type = TYPE.EDGES.INTER, - relation = I(c(as.list(rep("issue", 24)), as.list(rep("mail", 16)))) + relation = c(rep("issue", 24), rep("mail", 16)) ) ## remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - network.expected.data[["date"]] = unclass(network.expected.data[["date"]]) - network.expected.data[["artifact.type"]] = unclass(network.expected.data[["artifact.type"]]) network.expected.data[["message.id"]] = unclass(network.expected.data[["message.id"]]) network.expected.data[["thread"]] = unclass(network.expected.data[["thread"]]) network.expected.data[["issue.id"]] = unclass(network.expected.data[["issue.id"]]) network.expected.data[["event.name"]] = unclass(network.expected.data[["event.name"]]) - network.expected.data[["relation"]] = unclass(network.expected.data[["relation"]]) ## 3) build expected network network.expected = igraph::graph_from_data_frame(network.expected.data, vertices = vertices, - directed = net.conf$get.value("author.directed")) + directed = net.conf$get.value("author.directed")) + network.expected = convert.edge.attributes.to.list(network.expected) expect_true(igraph::identical_graphs(network.built, network.expected)) }) @@ -270,23 +265,22 @@ test_that("Construction of the multi network for the feature artifact with autho "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""), - date = I(as.list(get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", # author cochange - "2016-07-12 16:06:10", "2016-07-12 16:05:41", "2016-07-12 16:06:32", - "2016-07-12 16:06:10", "2016-07-12 16:06:32", - "2016-07-12 15:58:40", "2016-07-12 15:58:50", "2016-07-12 16:04:40", - "2016-07-12 16:05:37", - "2016-07-12 16:06:32", # artifact cochange - "2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", # bipartite cochange - "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", - "2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", # bipartite issue - "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", "2016-07-12 14:59:25", - "2016-07-12 16:02:30", "2016-07-12 16:06:01", "2016-07-15 19:55:39", "2017-05-23 12:32:39", - "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", - "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", - "2016-07-12 15:59:59", "2013-04-21 23:52:09", "2016-07-12 15:59:25", - "2016-07-12 16:03:59")))), - artifact.type = I(c(as.list(rep("Feature", 8)), as.list(rep("Mail", 4)), as.list(rep("Feature", 1)), as.list(rep("Feature", 6)), - as.list(rep("IssueEvent", 21)))), + date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", # author cochange + "2016-07-12 16:06:10", "2016-07-12 16:05:41", "2016-07-12 16:06:32", + "2016-07-12 16:06:10", "2016-07-12 16:06:32", + "2016-07-12 15:58:40", "2016-07-12 15:58:50", "2016-07-12 16:04:40", + "2016-07-12 16:05:37", + "2016-07-12 16:06:32", # artifact cochange + "2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", # bipartite cochange + "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", # bipartite issue + "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", "2016-07-12 14:59:25", + "2016-07-12 16:02:30", "2016-07-12 16:06:01", "2016-07-15 19:55:39", "2017-05-23 12:32:39", + "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", + "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", + "2016-07-12 15:59:59", "2013-04-21 23:52:09", "2016-07-12 15:59:25", + "2016-07-12 16:03:59")), + artifact.type = c(rep("Feature", 8), rep("Mail", 4), rep("Feature", 1), rep("Feature", 6), rep("IssueEvent", 21)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", # author cochange "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", @@ -310,8 +304,7 @@ test_that("Construction of the multi network for the feature artifact with autho as.list(rep(NA, 21)))), weight = 1, type = c(rep(TYPE.EDGES.INTRA, 13), rep(TYPE.EDGES.INTER, 27)), - relation = I(c(as.list(rep("cochange", 8)), as.list(rep("mail", 4)), as.list(rep("cochange", 1)), as.list(rep("cochange", 6)), - as.list(rep("issue", 21)))), + relation = c(rep("cochange", 8), rep("mail", 4), rep("cochange", 1), rep("cochange", 6), rep("issue", 21)), message.id = I(c(as.list(rep(NA, 8)), "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", @@ -331,12 +324,9 @@ test_that("Construction of the multi network for the feature artifact with autho ) ## Remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - edges[["date"]] = unclass(edges[["date"]]) - edges[["artifact.type"]] = unclass(edges[["artifact.type"]]) edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) - edges[["relation"]] = unclass(edges[["relation"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) edges[["thread"]] = unclass(edges[["thread"]]) edges[["author.name"]] = unclass(edges[["author.name"]]) @@ -345,7 +335,8 @@ test_that("Construction of the multi network for the feature artifact with autho ## 3) build expected network network.expected = igraph::graph_from_data_frame(edges, vertices = vertices, - directed = net.conf$get.value("author.directed")) + directed = net.conf$get.value("author.directed")) + network.expected = convert.edge.attributes.to.list(network.expected) assert.networks.equal(network.expected, network.built) }) @@ -403,31 +394,31 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "","","", "","", "", "","", ""), - date = I(as.list(get.date.from.string(c("2016-07-12 15:58:59 UTC", "2016-07-12 16:06:10 UTC", - "2016-07-12 16:00:45 UTC", "2016-07-12 16:05:41 UTC", - "2016-07-12 16:06:32 UTC", "2016-07-12 16:06:32 UTC", - "2013-05-05 21:46:30 UTC", "2013-05-05 21:49:21 UTC", - "2013-05-05 21:49:34 UTC", "2013-05-06 01:04:34 UTC", - "2013-05-25 03:48:41 UTC", "2013-05-25 04:08:07 UTC", - "2016-07-12 14:59:25 UTC", "2016-07-12 16:02:30 UTC", - "2016-07-12 16:06:01 UTC", "2016-07-15 19:55:39 UTC", - "2017-05-23 12:32:39 UTC", "2016-07-12 15:59:59 UTC", - "2016-07-15 20:07:47 UTC", "2016-07-27 20:12:08 UTC", - "2016-07-28 06:27:52 UTC", "2013-05-25 03:25:06 UTC", - "2013-05-25 06:06:53 UTC", "2013-05-25 06:22:23 UTC", - "2013-06-01 06:50:26 UTC", "2016-07-12 16:01:01 UTC", - "2016-07-12 16:02:02 UTC", "2013-04-21 23:52:09 UTC", - "2016-07-12 15:59:25 UTC", "2016-07-12 16:03:59 UTC")))), - artifact.type = I(c(as.list(rep("Feature", 6)), as.list(rep("IssueEvent", 24)))), + date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", + "2016-07-12 16:00:45", "2016-07-12 16:05:41", + "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2013-05-05 21:46:30", "2013-05-05 21:49:21", + "2013-05-05 21:49:34", "2013-05-06 01:04:34", + "2013-05-25 03:48:41", "2013-05-25 04:08:07", + "2016-07-12 14:59:25", "2016-07-12 16:02:30", + "2016-07-12 16:06:01", "2016-07-15 19:55:39", + "2017-05-23 12:32:39", "2016-07-12 15:59:59", + "2016-07-15 20:07:47", "2016-07-27 20:12:08", + "2016-07-28 06:27:52", "2013-05-25 03:25:06", + "2013-05-25 06:06:53", "2013-05-25 06:22:23", + "2013-06-01 06:50:26", "2016-07-12 16:01:01", + "2016-07-12 16:02:02", "2013-04-21 23:52:09", + "2016-07-12 15:59:25", "2016-07-12 16:03:59")), + artifact.type = c(rep("Feature", 6), rep("IssueEvent", 24)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 24)))), file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c", as.list(rep(NA, 24)))), artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 24)))), - weight = c(rep(1, 30)), - type = c(rep("Bipartite", 30)), - relation = I(c(as.list(rep("cochange", 6)), as.list(rep("issue", 24)))), + weight = 1, + type = "Bipartite", + relation = c(rep("cochange", 6), rep("issue", 24)), issue.id = I(c(as.list(rep(NA, 6)), "", "", "", "", "", "", @@ -441,16 +432,14 @@ test_that("Construction of the multi-artifact bipartite network with artifact re ) ## Remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - edges[["date"]] = unclass(edges[["date"]]) - edges[["artifact.type"]] = unclass(edges[["artifact.type"]]) edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) - edges[["relation"]] = unclass(edges[["relation"]]) edges[["issue.id"]] = unclass(edges[["issue.id"]]) edges[["event.name"]] = unclass(edges[["event.name"]]) net.expected = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) + net.expected = convert.edge.attributes.to.list(net.expected) assert.networks.equal(net.expected, net.combined) @@ -504,24 +493,24 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""), - date = I(as.list(get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32", - "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", - "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", - "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", - "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", - "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", - "2010-07-12 10:05:36")))), - artifact.type = I(c(as.list(rep("Feature", 6)), as.list(rep("Mail", 16)))), + date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", + "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", + "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", + "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", + "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", + "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", + "2010-07-12 10:05:36")), + artifact.type = c(rep("Feature", 6), rep("Mail", 16)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 16)))), file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c", as.list(rep(NA, 16)))), artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 16)))), - weight = rep(1,22), - type = rep("Bipartite", 22), - relation = I(c(as.list(rep("cochange", 6)), as.list(rep("mail", 16)))), + weight = 1, + type = "Bipartite", + relation = c(rep("cochange", 6), rep("mail", 16)), message.id = I(c(as.list(rep(NA, 6)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "", "", @@ -538,16 +527,14 @@ test_that("Construction of the multi-artifact bipartite network with artifact re ) ## Remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - edges[["date"]] = unclass(edges[["date"]]) - edges[["artifact.type"]] = unclass(edges[["artifact.type"]]) edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) - edges[["relation"]] = unclass(edges[["relation"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) edges[["thread"]] = unclass(edges[["thread"]]) net.expected = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) + net.expected = convert.edge.attributes.to.list(net.expected) assert.networks.equal(net.expected, net.combined) @@ -608,21 +595,21 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""), - date = I(as.list(get.date.from.string(c("2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", - "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", - "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", - "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2016-07-12 15:59:59", - "2016-07-15 20:07:47", "2016-07-27 20:12:08", "2016-07-28 06:27:52", - "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", - "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", - "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59", - "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", - "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", - "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", - "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", - "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", - "2010-07-12 10:05:36")))), - artifact.type = I(c(as.list(rep("IssueEvent", 24)), as.list(rep("Mail", 16)))), + date = get.date.from.string(c("2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", + "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", + "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", + "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2016-07-12 15:59:59", + "2016-07-15 20:07:47", "2016-07-27 20:12:08", "2016-07-28 06:27:52", + "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", + "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", + "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59", + "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", + "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", + "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", + "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", + "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", + "2010-07-12 10:05:36")), + artifact.type = c(rep("IssueEvent", 24), rep("Mail", 16)), issue.id = I(c("", "", "", "", "", "", "", "", "", @@ -632,9 +619,9 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "", "", as.list(rep(NA, 16)))), event.name = I(c(rep("commented", 24), as.list(rep(NA, 16)))), - weight = rep(1, 40), - type = rep("Bipartite", 40), - relation = I(c(as.list(rep("issue", 24)), as.list(rep("mail", 16)))), + weight = 1, + type = "Bipartite", + relation = c(rep("issue", 24), rep("mail", 16)), message.id = I(c(as.list(rep(NA, 24)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", @@ -643,23 +630,20 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", "<9b06e8d20801220234h659c18a3g95c12ac38248c7e0@mail.gmail.com>", - "<65a1sf31sagd684dfv31@mail.gmail.com>", "" - )), + "<65a1sf31sagd684dfv31@mail.gmail.com>", "")), thread = I(c(as.list(rep(NA, 24)), "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "")) ) ## Remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - edges[["date"]] = unclass(edges[["date"]]) - edges[["artifact.type"]] = unclass(edges[["artifact.type"]]) edges[["issue.id"]] = unclass(edges[["issue.id"]]) edges[["event.name"]] = unclass(edges[["event.name"]]) - edges[["relation"]] = unclass(edges[["relation"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) edges[["thread"]] = unclass(edges[["thread"]]) net.expected = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) + net.expected = convert.edge.attributes.to.list(net.expected) assert.networks.equal(net.expected, net.combined) @@ -730,32 +714,32 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "", "", "", "", "", "", "", "", "", ""), - date = I(as.list(get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32", - "2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", - "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", - "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", - "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2016-07-12 15:59:59", - "2016-07-15 20:07:47", "2016-07-27 20:12:08", "2016-07-28 06:27:52", - "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", - "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", - "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59", - "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", - "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", - "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", - "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", - "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", - "2010-07-12 10:05:36")))), - artifact.type = I(c(as.list(rep("Feature", 6)), as.list(rep("IssueEvent", 24)), as.list(rep("Mail", 16)))), + date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", + "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", + "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", + "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", + "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2016-07-12 15:59:59", + "2016-07-15 20:07:47", "2016-07-27 20:12:08", "2016-07-28 06:27:52", + "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", + "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", + "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59", + "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", + "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", + "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", + "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", + "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", + "2010-07-12 10:05:36")), + artifact.type = c(rep("Feature", 6), rep("IssueEvent", 24), rep("Mail", 16)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 40)))), file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c", as.list(rep(NA, 40)))), artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 40)))), - weight = rep(1, 46), - type = rep("Bipartite", 46), - relation = I(c(as.list(rep("cochange", 6)), as.list(rep("issue", 24)), as.list(rep("mail", 16)))), + weight = 1, + type = "Bipartite", + relation = c(rep("cochange", 6), rep("issue", 24), rep("mail", 16)), issue.id = I(c(as.list(rep(NA, 6)), "", "", "", "", "", "", "", "", @@ -781,18 +765,16 @@ test_that("Construction of the multi-artifact bipartite network with artifact re ) ## Remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - edges[["date"]] = unclass(edges[["date"]]) - edges[["artifact.type"]] = unclass(edges[["artifact.type"]]) edges[["hash"]] = unclass(edges[["hash"]]) edges[["file"]] = unclass(edges[["file"]]) edges[["artifact"]] = unclass(edges[["artifact"]]) - edges[["relation"]] = unclass(edges[["relation"]]) edges[["issue.id"]] = unclass(edges[["issue.id"]]) edges[["event.name"]] = unclass(edges[["event.name"]]) edges[["message.id"]] = unclass(edges[["message.id"]]) edges[["thread"]] = unclass(edges[["thread"]]) net.expected = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) + net.expected = convert.edge.attributes.to.list(net.expected) assert.networks.equal(net.expected, net.combined) diff --git a/tests/test-networks.R b/tests/test-networks.R index 463ebd5b..ff48676b 100644 --- a/tests/test-networks.R +++ b/tests/test-networks.R @@ -160,7 +160,7 @@ test_that("Simplify author-network with relation = c('cochange', 'mail') using b data$weight = rep(2, 6) data$type = rep(TYPE.EDGES.INTRA, 6) data$relation = list(list("cochange", "cochange"), list("cochange", "cochange"), list("cochange", "cochange"), - list("cochange", "cochange"), list("mail", "mail"), list("mail", "mail")) + list("cochange", "cochange"), list("mail", "mail"), list("mail", "mail")) data$message.id = list(as.list(rep(NA, 2)), as.list(rep(NA, 2)), as.list(rep(NA, 2)), as.list(rep(NA, 2)), list("<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>"), @@ -303,18 +303,18 @@ test_that("Simplify network with multi-relational edges", { ## create network with vertices connected by multi-relational edges data = data.frame(comb.1. = c("A", "B", "B", "C", "B", "B", "B", "C"), comb.2. = c("D", "E", "E", "F", "E", "E", "E", "F")) - data$relation = list("cochange", "cochange", - "mail", "mail", + data$relation = list(list("cochange"), list("cochange"), + list("mail"), list("mail"), list("mail", "cochange"), list("mail", "cochange", "mail"), list("mail", "mail"), list("mail", "mail")) ## build expected network - network.built = igraph::graph_from_data_frame(data, vertices = c("A", "B", "C", "D", "E", "F"), + network.base = igraph::graph_from_data_frame(data, vertices = c("A", "B", "C", "D", "E", "F"), directed = FALSE) ## ---------------------- simplify.multiple.relations == FALSE -------------------------- ## - network.built = simplify.network(network.built, simplify.multiple.relations = FALSE) + network.built = simplify.network(network.base, simplify.multiple.relations = FALSE) ## Note: (mail) can be simplified with (mail, mail). ## (mail) and (mail, mail) cannot be simplified with (mail, cochange). @@ -327,7 +327,7 @@ test_that("Simplify network with multi-relational edges", { data = data.frame(comb.1. = c("A", "B", "B", "C", "B"), comb.2. = c("D", "E", "E", "F", "E")) - data$relation = list("cochange", "cochange", + data$relation = list(list("cochange"), list("cochange"), list("mail", "mail", "mail"), list("mail", "mail", "mail"), list("mail", "cochange", "mail", "cochange", "mail")) network.expected = igraph::graph_from_data_frame(data, vertices = c("A", "B", "C", "D", "E", "F"), @@ -335,6 +335,26 @@ test_that("Simplify network with multi-relational edges", { assert.networks.equal(network.built, network.expected) + ## ---------------------- simplify.multiple.relations == TRUE --------------------------- ## + + network.built = simplify.network(network.base, simplify.multiple.relations = TRUE) + + ## Note: The order of partials in the B--E edge is determined + ## by collecting partials from all B--E source-edges top-down. + ## A -- (cochange) --> D + ## B -- (cochange, mail, mail, cochange, mail, cochange, mail, mail, mail) --> E + ## C -- (mail, mail, mail) --> F + + data = data.frame(comb.1. = c("A", "B", "C"), + comb.2. = c("D", "E", "F")) + data$relation = list(list("cochange"), + list("cochange", "mail", "mail", "cochange", "mail", "cochange", "mail", "mail", "mail"), + list("mail", "mail", "mail")) + network.expected = igraph::graph_from_data_frame(data, vertices = c("A", "B", "C", "D", "E", "F"), + directed = FALSE) + + assert.networks.equal(network.built, network.expected) + }) test_that("Remove isolated vertices", { @@ -1079,8 +1099,9 @@ patrick::with_parameters_test_that("Convert edge attributes to list", { } ## check edge attributes + to.list = function(attr) return(as.list(lapply(attr, as.list))); for (attr in igraph::edge_attr_names(network)) { - conversion.function = ifelse(attr %in% remain.as.is, identity, as.list) + conversion.function = ifelse(attr %in% remain.as.is, identity, to.list) expect_equal( conversion.function(igraph::edge_attr(network, attr)), igraph::edge_attr(network.listified, attr), From 894414a4a970822b9ecd59c0b6c480860707f636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 24 Feb 2025 19:03:20 +0100 Subject: [PATCH 17/92] Improve documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-split-network-time-based.R | 24 +++++++++++++++--------- util-networks.R | 3 +++ util-split.R | 6 ++++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/test-split-network-time-based.R b/tests/test-split-network-time-based.R index 9c4653d4..8d9d0e23 100644 --- a/tests/test-split-network-time-based.R +++ b/tests/test-split-network-time-based.R @@ -41,13 +41,16 @@ if (!dir.exists(CF.DATA)) CF.DATA = file.path(".", "tests", "codeface-data") #' Construct a (sub-)network with the same vertices and new edges #' -#' Note: This method is an adoption of \code{split.network.by.bins} with less functionality. +#' Note: This method is partially adopted from \code{split.network.by.bins}. +#' It replicates the way how new edges are set between the vertices +#' of an exisiting network (which is a useful functionality when testing +#' network splitting), but it does not perform any splitting logic. #' -#' @param network The network to extract the vertices from -#' @param edges The edges to add to the network [default: NULL] -#' @param remove.isolates Whether to remove isolated vertices [default: TRUE] +#' @param network the network to extract the vertices from +#' @param edges the edges to add to the network [default: NULL] +#' @param remove.isolates whether to remove isolated vertices [default: TRUE] #' -#' @return The partial edge +#' @return a new network with the vertices of the input network connnected by the new edges #' #' @seealso \code{split.network.by.bins} subnet.with.new.edges = function(network, edges = NULL, remove.isolates = TRUE) { @@ -80,12 +83,15 @@ subnet.with.new.edges = function(network, edges = NULL, remove.isolates = TRUE) #' Extract a partial edge from a simplified edge with multiple components #' -#' Note: This method is an adoption of \code{split.network.by.bins} with less functionality. +#' Note: This method is partially adopted from \code{split.network.by.bins}. +#' It replicates how partial edges can be extracted from a larger +#' multi-partial edge but it does so based on the indicees of +#' partials in the source edge instead of time-based bins. #' -#' @param edge The edge to extract the partial edge from -#' @param partials The indicess of the partial edge to extract +#' @param edge the edge to extract the partial edge from +#' @param partials the indicess of the partial edge to extract #' -#' @return The partial edge +#' @return the partial edge #' #' @seealso \code{split.network.by.bins} extract.partial.edge = function(edge, partials) { diff --git a/util-networks.R b/util-networks.R index 1cf207a6..d0167329 100644 --- a/util-networks.R +++ b/util-networks.R @@ -55,6 +55,9 @@ TYPE.EDGES.INTER = "Bipartite" ## Edge-attribute handling during simplification --------------------------- ## Edge-attribute contraction: configure handling of attributes by name +## Attention: If attributes are added or change to new aggregation strategies, this +## may influence logic in \code{split.network.by.bins}. For additional +## documentation, see \link{https://github.com/se-sic/coronet/pull/278}. EDGE.ATTR.HANDLING = list( ## network-analytic data weight = "sum", diff --git a/util-split.R b/util-split.R index 88533431..81d7cc4f 100644 --- a/util-split.R +++ b/util-split.R @@ -1274,13 +1274,15 @@ split.unify.range.names = function(ranges) { #' Note: As the last bound of a bin is exclusive, the end of the last bin is always #' set to \code{max(dates) + 1} to include the last date! #' -#' @param dates the dates that are to be split into several bins +#' @param dates the dates that are to be split into several bins either as a vector or as a (nested) list #' @param time.period the time period each bin lasts #' @param number.windows the number of consecutive time windows to get from this function. If set, #' the 'time.period' parameter is ignored. [default: NULL] #' #' @return a list, -#' the item 'vector': the bins each item in 'dates' belongs to, +#' the item 'vector': the bins each item in 'dates' belongs to. If 'dates' is +#' a nested list, the nesting structure is preserved and reflected in +#' the structure of the 'vector', #' the item 'bins': the bin labels, each spanning the length of 'time.period'; #' each item in the vector indicates the start of a bin, although the last #' item indicates the end of the last bin From 0fe32a259ef703c2de79135bfa6932a595fdc1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 3 Mar 2025 16:38:56 +0100 Subject: [PATCH 18/92] Remove 'is.list' check for dates retrieved from network edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'date' edge attribute is of list type by default an additional check is therefore not necessary. Signed-off-by: Maximilian Löffler --- util-split.R | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/util-split.R b/util-split.R index 81d7cc4f..2236eda5 100644 --- a/util-split.R +++ b/util-split.R @@ -546,13 +546,9 @@ split.network.time.based = function(network, time.period = "3 months", bins = NU sliding.window = FALSE ## find bins for given dates bins.date = get.date.from.string(bins) - if (is.list(dates)) { - bins.vector = lapply(dates, function(date) { - findInterval(date, bins.date, all.inside = FALSE) - }) - } else { - bins.vector = findInterval(dates, bins.date, all.inside = FALSE) - } + bins.vector = lapply(dates, function(date) { + findInterval(date, bins.date, all.inside = FALSE) + }) bins = seq_len(length(bins.date) - 1) # the last item just closes the last bin } From 09bd09adcaefa70b261cdd27e09fe7f2b85dd5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Wed, 12 Mar 2025 14:10:42 +0100 Subject: [PATCH 19/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/NEWS.md b/NEWS.md index ef3ad4e2..0a7f41a9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,18 @@ # coronet – Changelog +## unversioned + +### Added + +- Add the possibility to split networks that contain simplified edges (PR #278, 9798d33512dcdf50d3b88a1223fc4913a2a88eeb, 0ed437c14423c1917f1ba470e7e55db4626d380b, 67a6651b94d50cb7c2ab4a74888b0556d607b102, 98ef83158204be2a67b115cb25df5ba375cccf60, 7ec4d83fdeb308a24a350acd808941807b9511f1, 637d62ab70f098f26f241e588a99cdc49d10f56a, 2c70666f128f96a3a573f29a0cbbef14d803d193, 1cbc6fa36859d6db3a7ff4493ef19763e87d2de3, 41788ff029d038969bfc6b5773e919201c5ac595, b042c0dd08e2229514ccd25dee7a119f25b1ab45, 36d23d657f412aa1953c4773076e593273f19d8e, 402c256d9a05e4ffb297d4ea1fc25d0230787bc0, 54af2b19a112070f10d191b98b055482748426a7, 894414a4a970822b9ecd59c0b6c480860707f636, 0fe32a259ef703c2de79135bfa6932a595fdc1c5) + +### Changed/Improved + +- For consistency reasons: Ensure that the values of edge attributes are always lists even when they represent singular values (PR #278, 6fae1843740ed8e48c89c2ee4e61f995b5d0b8f5, 416c817998540fc0b82d9959574838b571b4d6fb) + +### Fixed + ## 5.0 ### Announcement From 5aa4e4193f0c00095fedf961c6060a5c035ef9c6 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 28 Jan 2025 17:35:55 +0100 Subject: [PATCH 20/92] Add basic implementation of stemming first draft of default function for stemming including preprocessing steps Signed-off-by: Leo Sendelbach --- install.R | 4 +++- util-data-misc.R | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/install.R b/install.R index 1af5c35a..fd1de180 100644 --- a/install.R +++ b/install.R @@ -50,7 +50,9 @@ packages = c( "purrr", "testthat", "patrick", - "covr" + "covr", + "tm", + "SnowballC" ) diff --git a/util-data-misc.R b/util-data-misc.R index 61d5bb84..1e15cf9a 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -28,6 +28,8 @@ requireNamespace("sqldf") # for SQL-selections on data.frames requireNamespace("logging") # for logging +requireNamespace("tm") # for NLP functionalities +requireNamespace("SnowballC") # for stemming #' Helper function to mask all issues in the issue data frame. #' @@ -768,3 +770,56 @@ get.issue.is.pull.request = function(proj.data) { logging::logdebug("get.issue.is.pull.request: finished") return(issue.id.to.is.pr) } + +## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / +## Commit Message Functionalities ------------------------------------------ + +#' +#' +get.stemmed.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { + preprocessing = match.arg.or.default(preprocessing, several.ok = TRUE) + stemmed.messages = create.empty.data.frame(c("hash", "stemmed.message")) + ## get commit message data of the given hashes + ## if no hashes are given consider all commits + commit.message.data = proj.data$get.commit.messages() + if (!is.null(commit.hashes)) { + commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + } + + ## if data is empty, abort process + if (nrow(commit.message.data) < 1) { + return(stemmed.messages) + } + + ## create a corpus with all selected commit messages + messages = c() + for (i in seq_len(nrow(commit.message.data))) { + messages = c(messages, paste(commit.message.data[i, "title"], commit.message.data[i, "message"])) + } + corpus = tm::Corpus(tm::VectorSource(messages)) + + ## preprocessing steps + if ("lowercase" %in% preprocessing) { + ## convert to lowercase + corpus = tm::tm_map(corpus, tm::content_transformer(tolower)) + } + if ("punctuation" %in% preprocessing) { + ## remove punctuation + corpus = tm::tm_map(corpus, tm::removePunctuation) + } + if ("stopwords" %in% preprocessing) { + ## remove stopwords + corpus = tm::tm_map(corpus, tm::removeWords, tm::stopwords("english")) + } + if ("whitespaces" %in% preprocessing) { + ## remove excess whitespaces + corpus = tm::tm_map(corpus, tm::stripWhitespace) + } + ## apply stemming + corpus = tm::tm_map(corpus, tm::stemDocument) + ## create output dataframe + for (i in seq_len(nrow(commit.message.data))) { + stemmed.messages[i,] = c(commit.message.data[["hash"]][i], corpus$content[i]) + } + return(stemmed.messages) +} \ No newline at end of file From 99f0638566c0062b987617bc3fe3ace1db7729ee Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 11:41:24 +0100 Subject: [PATCH 21/92] Add methods for commit message functionalities refactor preprocessing steps to be in their own method, add tokenization, lemmatization, keyword search and token count functionalities Signed-off-by: Leo Sendelbach --- install.R | 5 +- util-data-misc.R | 154 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/install.R b/install.R index fd1de180..872c5c9a 100644 --- a/install.R +++ b/install.R @@ -19,7 +19,7 @@ ## Copyright 2020-2024 by Thomas Bock ## Copyright 2019 by Anselm Fehnker ## Copyright 2021 by Christian Hechtl -## Copyright 2024 by Leo Sendelbach +## Copyright 2024-2025 by Leo Sendelbach ## Copyright 2024 by Maximilian Löffler ## All Rights Reserved. ## @@ -52,7 +52,8 @@ packages = c( "patrick", "covr", "tm", - "SnowballC" + "SnowballC", + "textstem" ) diff --git a/util-data-misc.R b/util-data-misc.R index 1e15cf9a..b3cb19d5 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -21,6 +21,7 @@ ## Copyright 2021 by Christian Hechtl ## Copyright 2022 by Jonathan Baumann ## Copyright 2024 by Thomas Bock +## Copyright 2025 by Leo Sendelbach ## All Rights Reserved. ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / @@ -774,11 +775,16 @@ get.issue.is.pull.request = function(proj.data) { ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / ## Commit Message Functionalities ------------------------------------------ +#' Apply preprocessing steps to commit messages of given commits #' +#' @param proj.data the \code{ProjectData} containing the commit message data +#' @param commit.hashes the hashes of the commits that should be considered +#' @param preprocessing the preprocessing steps to be executed (all enabled by default) #' -get.stemmed.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { +#' @return a dataframe containing the hashes and preprocesessed messages +get.preprocessed.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { preprocessing = match.arg.or.default(preprocessing, several.ok = TRUE) - stemmed.messages = create.empty.data.frame(c("hash", "stemmed.message")) + preprocessed.messages = create.empty.data.frame(c("hash", "preprocessed.message")) ## get commit message data of the given hashes ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() @@ -788,13 +794,17 @@ get.stemmed.commit.messages = function(proj.data, commit.hashes = NULL, preproce ## if data is empty, abort process if (nrow(commit.message.data) < 1) { - return(stemmed.messages) + return(preprocessed.messages) } ## create a corpus with all selected commit messages messages = c() for (i in seq_len(nrow(commit.message.data))) { - messages = c(messages, paste(commit.message.data[i, "title"], commit.message.data[i, "message"])) + current = commit.message.data[i, "title"] + if (proj.data$get.project.conf.entry("commit.messages") == "message") { + current = paste(current, commit.message.data[i, "message"]) + } + messages = c(messages, current) } corpus = tm::Corpus(tm::VectorSource(messages)) @@ -815,11 +825,141 @@ get.stemmed.commit.messages = function(proj.data, commit.hashes = NULL, preproce ## remove excess whitespaces corpus = tm::tm_map(corpus, tm::stripWhitespace) } + + ## create output dataframe + for (i in seq_len(nrow(commit.message.data))) { + preprocessed.messages[i,] = c(commit.message.data[["hash"]][i], corpus$content[i]) + } + + return(preprocessed.messages) +} + +#' Apply stemming to commit messages of given commits. Preprocessing will be executed as part of this. +#' +#' @param proj.data the \code{ProjectData} containing the commit message data +#' @param commit.hashes the hashes of the commits that should be considered +#' @param preprocessing the preprocessing steps to be executed (all enabled by default) +#' +#' @return a dataframe containing the hashes and stemmed messages +get.stemmed.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { + ## apply preprocessing + preprocessed.messages = get.preprocessed.messages(proj.data, commit.hashes, preprocessing) + stemmed.messages = create.empty.data.frame(c("hash", "stemmed.message")) + ## build corpus + corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[,"preprocessed.message"])) ## apply stemming corpus = tm::tm_map(corpus, tm::stemDocument) ## create output dataframe - for (i in seq_len(nrow(commit.message.data))) { - stemmed.messages[i,] = c(commit.message.data[["hash"]][i], corpus$content[i]) + for (i in seq_len(nrow(preprocessed.messages))) { + stemmed.messages[i,] = c(preprocessed.messages[["hash"]][i], corpus$content[i]) } return(stemmed.messages) -} \ No newline at end of file +} + +#' Apply tokenization to commit messages of given commits. +#' +#' @param proj.data the \code{ProjectData} containing the commit message data +#' @param commit.hashes the hashes of the commits that should be considered +#' +#' @return a list of vectors containing the tokens from the commit messages +get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { + ## get commit message data of the given hashes + ## if no hashes are given consider all commits + commit.message.data = proj.data$get.commit.messages() + if (!is.null(commit.hashes)) { + commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + } + tokens = list() + for (i in seq_len(nrow(commit.message.data))) { + current = commit.message.data[i, "title"] + if (proj.data$get.project.conf.entry("commit.messages") == "message") { + current = paste(current, commit.message.data[i, "message"]) + } + ## add tokens to result + tokens[[length(tokens)+1]] = tm::Boost_tokenizer(current) + } + + return(tokens) +} + +#' Apply lemmatization to commit messages of given commits. Preprocessing will be executed as part of this. +#' +#' @param proj.data the \code{ProjectData} containing the commit message data +#' @param commit.hashes the hashes of the commits that should be considered +#' @param preprocessing the preprocessing steps to be executed (all enabled by default) +#' +#' @return a dataframe containing the hashes and lemmatized messages +get.lemmatized.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { + ## apply preprocessing + preprocessed.messages = get.preprocessed.messages(proj.data, commit.hashes, preprocessing) + lemmatized.messages = create.empty.data.frame(c("hash", "lemmatized.message")) + ## build corpus + corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[,"preprocessed.message"])) + ## apply lemmatization + corpus = tm::tm_map(corpus, textstem::lemmatize_strings) + ## create output dataframe + for (i in seq_len(nrow(preprocessed.messages))) { + lemmatized.messages[i,] = c(preprocessed.messages[["hash"]][i], corpus$content[i]) + } + return(lemmatized.messages) +} + +#' Get Commits messages that match given strings. +#' +#' @param proj.data the \code{ProjectData} containing the commit message data +#' @param commit.hashes the hashes of the commits that should be considered +#' @param strings the strings that are searched for +#' @param match the method which describes how many of the strings need to be in a message in order for +#' that message to be returned (default: any) +#' +#' @return a dataframe containing the hashes and matching messages +get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strings, match = any) { + messages = create.empty.data.frame(c("hash", "message")) + ## get commit message data of the given hashes + ## if no hashes are given consider all commits + commit.message.data = proj.data$get.commit.messages() + if (!is.null(commit.hashes)) { + commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + } + + for (i in seq_len(nrow(commit.message.data))) { + current = commit.message.data[i, "title"] + if (proj.data$get.project.conf.entry("commit.messages") == "message") { + current = paste(current, commit.message.data[i, "message"]) + } + ## check if message contains strings + check = lapply(strings, function(word) { + return (grepl(word, current, ignore.case = TRUE)) + }) + if (match(check)) { + messages[nrow(messages)+1,] = c(commit.message.data[["hash"]][i], current) + } + } + return(messages) +} + + +#' Count tokens in given commit messages. +#' +#' @param proj.data the \code{ProjectData} containing the commit message data +#' @param commit.hashes the hashes of the commits that should be considered +#' +#' @return a dataframe containing the hashes and token counts +get.commit.message.counts = function(proj.data, commit.hashes = NULL) { + messages = create.empty.data.frame(c("hash", "count")) + ## get commit message data of the given hashes + ## if no hashes are given consider all commits + commit.message.data = proj.data$get.commit.messages() + if (!is.null(commit.hashes)) { + commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + } + ## get tokens + tokens = get.tokenized.commit.messages(proj.data, commit.hashes) + + for (i in seq_len(nrow(commit.message.data))) { + hash = commit.message.data[["hash"]][i] + ## count tokens + messages[nrow(messages)+1,] = c(hash, length(tokens[[i]])) + } + return(messages) +} From 25cb48a64dad68157fc5dd3e12862453f25131a0 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 12:00:17 +0100 Subject: [PATCH 22/92] add tests for new functionalities add new file 'test-data-misc' for tests, incomplete Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test-data-misc.R diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R new file mode 100644 index 00000000..9fa9318b --- /dev/null +++ b/tests/test-data-misc.R @@ -0,0 +1,58 @@ +## This file is part of coronet, which is free software: you +## can redistribute it and/or modify it under the terms of the GNU General +## Public License as published by the Free Software Foundation, version 2. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +## +## Copyright 2025 by Leo Sendelbach +## All Rights Reserved. + + +context("Tests for the file 'util-core-peripheral.R'") + +## +## Context +## + +CF.DATA = file.path(".", "codeface-data") +CF.SELECTION.PROCESS = "testing" +CASESTUDY = "test" +ARTIFACT = "feature" + +## use only when debugging this file independently +if (!dir.exists(CF.DATA)) CF.DATA = file.path(".", "tests", "codeface-data") + +## Prepare global setting +proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) + +test_that("Commit message preprocessing steps: Whitespace removal", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.messages(proj.data, preprocessing = "whitespaces") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + preprocessed.message = c("Add stuff ", + "Add some more stuff ", + "I added important things the things are nothing", + "I wish it would work now ", + "Wish intensifies", + "... still doesn't work as expected", + " ")) + ## Assert + + expect_equal(expected, result) +}) From e469d3a0cf2881c378469b6ccfea9c204d13f19b Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 16:35:31 +0100 Subject: [PATCH 23/92] Add trimming to preprocessing Also change order in 'install.R' Signed-off-by: Leo Sendelbach --- install.R | 8 ++++---- tests/test-data-misc.R | 15 +++++++-------- util-data-misc.R | 3 +++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/install.R b/install.R index 872c5c9a..3b86d859 100644 --- a/install.R +++ b/install.R @@ -48,12 +48,12 @@ packages = c( "Matrix", "fastmap", "purrr", - "testthat", - "patrick", - "covr", "tm", + "textstem", "SnowballC", - "textstem" + "testthat", + "patrick", + "covr" ) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 9fa9318b..bf06155d 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -45,14 +45,13 @@ test_that("Commit message preprocessing steps: Whitespace removal", { "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", "0a1a5c523d835459c42f33e863623138555e2526"), - preprocessed.message = c("Add stuff ", - "Add some more stuff ", - "I added important things the things are nothing", - "I wish it would work now ", - "Wish intensifies", - "... still doesn't work as expected", - " ")) + preprocessed.message = c("Add stuff", + "Add some more stuff", + "I added important things the things are nothing", + "I wish it would work now", + "Wish intensifies", + "... still doesn't work as expected", + "")) ## Assert - expect_equal(expected, result) }) diff --git a/util-data-misc.R b/util-data-misc.R index b3cb19d5..ac9f96ad 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -826,6 +826,9 @@ get.preprocessed.messages = function(proj.data, commit.hashes = NULL, preprocess corpus = tm::tm_map(corpus, tm::stripWhitespace) } + ## trim leading and trailing spaces + corpus = tm::tm_map(corpus, trimws) + ## create output dataframe for (i in seq_len(nrow(commit.message.data))) { preprocessed.messages[i,] = c(commit.message.data[["hash"]][i], corpus$content[i]) From fe5495d65636514ba63c3e10975bde00a8c89f3f Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 16:53:42 +0100 Subject: [PATCH 24/92] Add tests for preprocessing Signel test for each step and one for combination Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 119 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index bf06155d..4428e6ac 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -32,6 +32,80 @@ if (!dir.exists(CF.DATA)) CF.DATA = file.path(".", "tests", "codeface-data") ## Prepare global setting proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) +test_that("Commit message preprocessing steps: Lowercase transformation", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.messages(proj.data, preprocessing = "lowercase") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + preprocessed.message = c("add stuff", + "add some more stuff", + "i added important things the things are\nnothing", + "i wish it would work now", + "wish intensifies", + "... still\ndoesn't\nwork\nas expected", + "")) + + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message preprocessing steps: Punctuation removal", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.messages(proj.data, preprocessing = "punctuation") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + preprocessed.message = c("Add stuff", + "Add some more stuff", + "I added important things the things are\nnothing", + "I wish it would work now", + "Wish intensifies", + "still\ndoesnt\nwork\nas expected", + "")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message preprocessing steps: Stopword removal", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.messages(proj.data, preprocessing = "stopwords") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + preprocessed.message = c("Add stuff", + "Add stuff", + "I added important things things \nnothing", + "I wish work now", + "Wish intensifies", + "... still\n\nwork\n expected", + "")) + + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message preprocessing steps: Whitespace removal", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) @@ -55,3 +129,48 @@ test_that("Commit message preprocessing steps: Whitespace removal", { ## Assert expect_equal(expected, result) }) + +test_that("Commit message preprocessing steps: All preprocesing", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.messages(proj.data) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + preprocessed.message = c("add stuff", + "add stuff", + "added important things things nothing", + "wish work now", + "wish intensifies", + "still doesnt work expected", + "")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message preprocessing steps: limited commit number", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.messages(proj.data, commit.hashes = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526")) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + preprocessed.message = c("add stuff", + "add stuff", + "still doesnt work expected", + "")) + ## Assert + expect_equal(expected, result) +}) From cffad2e71230c1ae51591053842f2f06316ba485 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 17:02:28 +0100 Subject: [PATCH 25/92] Add tests for stemming commit messages One test with only lowercase, one with all preprocessing steps Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 4428e6ac..ba217959 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -174,3 +174,51 @@ test_that("Commit message preprocessing steps: limited commit number", { ## Assert expect_equal(expected, result) }) + +test_that("Commit message stemming: only lowercase preprocessing", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.stemmed.commit.messages(proj.data, preprocessing = "lowercase") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + stemmed.message = c("add stuff", + "add some more stuff", + "i ad import thing the thing are noth", + "i wish it would work now", + "wish intensifi", + "... still doesn't work as expect", + "")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message stemming: All preprocesing", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.stemmed.commit.messages(proj.data) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + stemmed.message = c("add stuff", + "add stuff", + "ad import thing thing noth", + "wish work now", + "wish intensifi", + "still doesnt work expect", + "")) + ## Assert + expect_equal(expected, result) +}) From bb744082e3cf083801aea6f671dc86f4780c506a Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 17:13:24 +0100 Subject: [PATCH 26/92] Add test for tokenization Single test as there is no preprocessing for tokenization Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index ba217959..731730de 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -222,3 +222,20 @@ test_that("Commit message stemming: All preprocesing", { ## Assert expect_equal(expected, result) }) + +test_that("Commit message tokenization", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.tokenized.commit.messages(proj.data) + + ## Act + expected = list(c("Add", "stuff"), + c("Add", "some", "more", "stuff"), + c("I", "added", "important", "things", "the", "things", "are", "nothing"), + c("I", "wish", "it", "would", "work", "now"), + c("Wish", "intensifies"), + c("...", "still", "doesn't", "work", "as", "expected"), + character(0)) + ## Assert + expect_equal(expected, result) +}) From 10b57546eae2f71a968d33265da45ffc13ed9286 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 17:18:43 +0100 Subject: [PATCH 27/92] Add tests for lemmatizing commit messages One test with only lowercase preprocessing, one with all preprocessing steps Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 731730de..1d2da68c 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -239,3 +239,51 @@ test_that("Commit message tokenization", { ## Assert expect_equal(expected, result) }) + +test_that("Commit message lemmatization: only lowercase preprocessing", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.lemmatized.commit.messages(proj.data, preprocessing = "lowercase") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + lemmatized.message = c("add stuff", + "add some much stuff", + "i add important thing the thing be nothing", + "i wish it would work now", + "wish intensify", + "... still doesn't work as expect", + "")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message lemmatization: All preprocesing", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.lemmatized.commit.messages(proj.data) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + lemmatized.message = c("add stuff", + "add stuff", + "add important thing thing nothing", + "wish work now", + "wish intensify", + "still doesnt work expect", + "")) + ## Assert + expect_equal(expected, result) +}) From 2d25d7b0cd9f6189730a9199a8e67f8f230139d1 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 17:49:02 +0100 Subject: [PATCH 28/92] Add tests for keyword search multiple tests for different match functions: any, all and a custom function Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 78 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 1d2da68c..03dfe7f3 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -287,3 +287,81 @@ test_that("Commit message lemmatization: All preprocesing", { ## Assert expect_equal(expected, result) }) + +test_that("Commit message keyword search: any match, single string", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, strings = "add") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774"), + message = c("Add stuff ", + "Add some more stuff ", + "I added important things the things are\nnothing")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message keyword search: any match, multiple strings", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, strings = c("add", "intensifies")) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f"), + message = c("Add stuff ", + "Add some more stuff ", + "I added important things the things are\nnothing", + "Wish intensifies")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message keyword search: all match, multiple strings, no result", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, strings = c("add", "intensifies"), match = all) + + ## Act + expected = create.empty.data.frame(c("hash", "message")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message keyword search: all match, multiple strings", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, strings = c("add", "stuff"), match = all) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338"), + message = c("Add stuff ", + "Add some more stuff ")) + ## Assert + expect_equal(expected, result) +}) + +test_that("Commit message keyword search: at least 2 match, multiple strings", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, strings = c("add", "stuff", "I "), + match = function(x) { + return (sum(unlist(x)) >= 2) + }) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774"), + message = c("Add stuff ", + "Add some more stuff ", + "I added important things the things are\nnothing")) + ## Assert + expect_equal(expected, result) +}) From 228fba6a213f6a93d23f7ce19abca509f2d9e431 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 4 Mar 2025 17:57:11 +0100 Subject: [PATCH 29/92] Add test for commit message token counts Single test as all functionality is covered Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 03dfe7f3..3f9ebf5e 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -365,3 +365,22 @@ test_that("Commit message keyword search: at least 2 match, multiple strings", { ## Assert expect_equal(expected, result) }) + +test_that("Commit message token counts", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.message.counts(proj.data) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + count = c("2", "4", "8", "6", "2", "6", "0")) + + ## Assert + expect_equal(expected, result) +}) From bd3e2c1a9fe1cf9238105bd5f8b808988e42319a Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 11 Mar 2025 16:09:31 +0100 Subject: [PATCH 30/92] Update required Matrix version force matrix version 1.5.0 or higher as required for textstem to work Signed-off-by: Leo Sendelbach --- install.R | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/install.R b/install.R index 3b86d859..57216d46 100644 --- a/install.R +++ b/install.R @@ -79,11 +79,14 @@ if (length(p) > 0) { } Matrix.version = installed.packages()[rownames(installed.packages()) == "Matrix", "Version"] - if (compareVersion(Matrix.version, "1.3.0") == -1) { - print("WARNING: Matrix version 1.3.0 or higher is necessary for using coronet. Re-install package Matrix...") - install.packages("Matrix", dependencies = NA, verbose = TRUE, quiet = TRUE) + if (compareVersion(Matrix.version, "1.5.0") == -1) { + print("WARNING: Matrix version 1.5.0 or higher is necessary for using coronet. Re-install package Matrix...") + matrix.1.5.4.url = "https://cran.r-project.org/src/contrib/Archive/Matrix/Matrix_1.5-4.tar.gz" + install.packages(matrix.1.5.4.url, repos = NULL, dependencies = NA, verbose = TRUE, quiet = TRUE) + ## redo installation of textstem, which fails if matrix is outdated or not present + install.packages("textstem", dependencies = NA, verbose = TRUE, quiet = TRUE) Matrix.version = installed.packages()[rownames(installed.packages()) == "Matrix", "Version"] - if (compareVersion(Matrix.version, "1.3.0") == -1) { + if (compareVersion(Matrix.version, "1.5.0") == -1) { print("WARNING: Re-installation of package Matrix did not end up in the necessary package version.") } } From 37c419ea3ae538ba3cbd2b46ec19a3637289235f Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 11 Mar 2025 16:20:19 +0100 Subject: [PATCH 31/92] Add additional tests now also testing the possible configuration of only title vs full message Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 3f9ebf5e..7b5e62da 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -57,6 +57,31 @@ test_that("Commit message preprocessing steps: Lowercase transformation", { expect_equal(expected, result) }) +test_that("Commit message preprocessing steps: Lowercase transformation, only title", { + proj.conf$update.value("commit.messages", "title") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.messages(proj.data, preprocessing = "lowercase") + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + preprocessed.message = c("add stuff", + "add some more stuff", + "i added important things", + "i wish it would work now", + "wish", + "...", + "")) + + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message preprocessing steps: Punctuation removal", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) @@ -240,6 +265,24 @@ test_that("Commit message tokenization", { expect_equal(expected, result) }) +test_that("Commit message tokenization, only title", { + proj.conf$update.value("commit.messages", "title") + proj.data = ProjectData$new(proj.conf) + result = get.tokenized.commit.messages(proj.data) + + ## Act + expected = list(c("Add", "stuff"), + c("Add", "some", "more", "stuff"), + c("I", "added", "important", "things"), + c("I", "wish", "it", "would", "work", "now"), + c("Wish"), + c("..."), + character(0)) + + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message lemmatization: only lowercase preprocessing", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) @@ -322,6 +365,22 @@ test_that("Commit message keyword search: any match, multiple strings", { expect_equal(expected, result) }) +test_that("Commit message keyword search: any match, multiple strings, only title", { + proj.conf$update.value("commit.messages", "title") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, strings = c("add", "intensifies")) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774"), + message = c("Add stuff", + "Add some more stuff", + "I added important things")) + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message keyword search: all match, multiple strings, no result", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) From 7d8fd39f164c776921e3fb36daf79256e7be7426 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 11 Mar 2025 16:47:12 +0100 Subject: [PATCH 32/92] Change method name change method name from 'get.preprocessed.messages' to 'get.preprocessed.commit.messages' for consistency Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 14 +++++++------- util-data-misc.R | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 7b5e62da..1f63962f 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -35,7 +35,7 @@ proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) test_that("Commit message preprocessing steps: Lowercase transformation", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) - result = get.preprocessed.messages(proj.data, preprocessing = "lowercase") + result = get.preprocessed.commit.messages(proj.data, preprocessing = "lowercase") ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", @@ -60,7 +60,7 @@ test_that("Commit message preprocessing steps: Lowercase transformation", { test_that("Commit message preprocessing steps: Lowercase transformation, only title", { proj.conf$update.value("commit.messages", "title") proj.data = ProjectData$new(proj.conf) - result = get.preprocessed.messages(proj.data, preprocessing = "lowercase") + result = get.preprocessed.commit.messages(proj.data, preprocessing = "lowercase") ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", @@ -85,7 +85,7 @@ test_that("Commit message preprocessing steps: Lowercase transformation, only ti test_that("Commit message preprocessing steps: Punctuation removal", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) - result = get.preprocessed.messages(proj.data, preprocessing = "punctuation") + result = get.preprocessed.commit.messages(proj.data, preprocessing = "punctuation") ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", @@ -109,7 +109,7 @@ test_that("Commit message preprocessing steps: Punctuation removal", { test_that("Commit message preprocessing steps: Stopword removal", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) - result = get.preprocessed.messages(proj.data, preprocessing = "stopwords") + result = get.preprocessed.commit.messages(proj.data, preprocessing = "stopwords") ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", @@ -134,7 +134,7 @@ test_that("Commit message preprocessing steps: Stopword removal", { test_that("Commit message preprocessing steps: Whitespace removal", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) - result = get.preprocessed.messages(proj.data, preprocessing = "whitespaces") + result = get.preprocessed.commit.messages(proj.data, preprocessing = "whitespaces") ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", @@ -158,7 +158,7 @@ test_that("Commit message preprocessing steps: Whitespace removal", { test_that("Commit message preprocessing steps: All preprocesing", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) - result = get.preprocessed.messages(proj.data) + result = get.preprocessed.commit.messages(proj.data) ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", @@ -182,7 +182,7 @@ test_that("Commit message preprocessing steps: All preprocesing", { test_that("Commit message preprocessing steps: limited commit number", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) - result = get.preprocessed.messages(proj.data, commit.hashes = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + result = get.preprocessed.commit.messages(proj.data, commit.hashes = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", "0a1a5c523d835459c42f33e863623138555e2526")) diff --git a/util-data-misc.R b/util-data-misc.R index ac9f96ad..eeedd990 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -782,7 +782,7 @@ get.issue.is.pull.request = function(proj.data) { #' @param preprocessing the preprocessing steps to be executed (all enabled by default) #' #' @return a dataframe containing the hashes and preprocesessed messages -get.preprocessed.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { +get.preprocessed.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { preprocessing = match.arg.or.default(preprocessing, several.ok = TRUE) preprocessed.messages = create.empty.data.frame(c("hash", "preprocessed.message")) ## get commit message data of the given hashes @@ -846,7 +846,7 @@ get.preprocessed.messages = function(proj.data, commit.hashes = NULL, preprocess #' @return a dataframe containing the hashes and stemmed messages get.stemmed.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { ## apply preprocessing - preprocessed.messages = get.preprocessed.messages(proj.data, commit.hashes, preprocessing) + preprocessed.messages = get.preprocessed.commit.messages(proj.data, commit.hashes, preprocessing) stemmed.messages = create.empty.data.frame(c("hash", "stemmed.message")) ## build corpus corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[,"preprocessed.message"])) @@ -894,7 +894,7 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { #' @return a dataframe containing the hashes and lemmatized messages get.lemmatized.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { ## apply preprocessing - preprocessed.messages = get.preprocessed.messages(proj.data, commit.hashes, preprocessing) + preprocessed.messages = get.preprocessed.commit.messages(proj.data, commit.hashes, preprocessing) lemmatized.messages = create.empty.data.frame(c("hash", "lemmatized.message")) ## build corpus corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[,"preprocessed.message"])) From 4896eab1f3e92c2233d7f0be1d6827f475fdf47e Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 11 Mar 2025 16:49:10 +0100 Subject: [PATCH 33/92] Change Matrix version again Since version 1.5.4 did not work, change matrix version to 1.5.0 Signed-off-by: Leo Sendelbach --- install.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.R b/install.R index 57216d46..90e5db8e 100644 --- a/install.R +++ b/install.R @@ -81,8 +81,8 @@ if (length(p) > 0) { Matrix.version = installed.packages()[rownames(installed.packages()) == "Matrix", "Version"] if (compareVersion(Matrix.version, "1.5.0") == -1) { print("WARNING: Matrix version 1.5.0 or higher is necessary for using coronet. Re-install package Matrix...") - matrix.1.5.4.url = "https://cran.r-project.org/src/contrib/Archive/Matrix/Matrix_1.5-4.tar.gz" - install.packages(matrix.1.5.4.url, repos = NULL, dependencies = NA, verbose = TRUE, quiet = TRUE) + matrix.1.5.0.url = "https://cran.r-project.org/src/contrib/Archive/Matrix/Matrix_1.5-0.tar.gz" + install.packages(matrix.1.5.0.url, repos = NULL, dependencies = NA, verbose = TRUE, quiet = TRUE) ## redo installation of textstem, which fails if matrix is outdated or not present install.packages("textstem", dependencies = NA, verbose = TRUE, quiet = TRUE) Matrix.version = installed.packages()[rownames(installed.packages()) == "Matrix", "Version"] From d5fa0d40e0f08a6c5961f42a952674b46997ece8 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 11 Mar 2025 16:50:34 +0100 Subject: [PATCH 34/92] Add new functionality to 'README.md' added new section in 'additional functionalities' Signed-off-by: Leo Sendelbach --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 4552725c..84239187 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ If you wonder: The name `coronet` derives as an acronym from the words "configur - [Core/Peripheral classification](#coreperipheral-classification) - [Count-based metrics](#count-based-metrics) - [Network-based metrics](#network-based-metrics) + - [Commit message functionalities](#commit-message-functionalities) - [How-to](#how-to) - [File/Module overview](#filemodule-overview) - [Configuration classes](#configuration-classes) @@ -429,6 +430,12 @@ In this section, we provide descriptions of the different algorithms we provide * calculates scores based on the eccentricity of vertices in a network * eccentricity measures the length of the shortest path to each vertex's furthest reachable vertex +#### Commit message functionalities + +In this section, we give an overview of the functionalities we offer regarding commit messages. These consist of basic NLP tasks, such as stemming (`get.stemmed.commit.messages`), tokenization (`get.tokenized.commit.messages`), and +lemmatization (`get.lemmatized.commit.messages`), as well as preprocessing steps (`get.preprocessed.commit.messages`) such as lowercase transformation and removal of punctuation, stopwords and extra whitespaces. Apart from these, +there is the option of using a set of strings to search for matching commits (`get.commit.messages.by.strings`) as well as getting token counts for commit messages (`get.commit.message.counts`). + ### How-to In this section, we give a short example on how to initialize all needed objects and build a bipartite network. From 84d9a57e89e5962225518391f33e4f8b6edff0c6 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 25 Mar 2025 15:28:27 +0100 Subject: [PATCH 35/92] Add tests for missing lines Add tests for selection of specific commits and empty selection where missing Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 1f63962f..90deb6b1 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -179,6 +179,17 @@ test_that("Commit message preprocessing steps: All preprocesing", { expect_equal(expected, result) }) +test_that("Commit message preprocessing steps: All preprocesing but nonexisting commit", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.commit.messages(proj.data, commit.hashes = c("1234567890123456789012345678901234567890")) + + ## Act + expected = create.empty.data.frame(c("hash", "preprocessed.message")) + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message preprocessing steps: limited commit number", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) @@ -265,6 +276,19 @@ test_that("Commit message tokenization", { expect_equal(expected, result) }) +test_that("Commit message tokenization, only 2 commits", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.tokenized.commit.messages(proj.data, c("3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61")) + + ## Act + expected = list(c("I", "added", "important", "things", "the", "things", "are", "nothing"), + c("I", "wish", "it", "would", "work", "now")) + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message tokenization, only title", { proj.conf$update.value("commit.messages", "title") proj.data = ProjectData$new(proj.conf) @@ -347,6 +371,23 @@ test_that("Commit message keyword search: any match, single string", { expect_equal(expected, result) }) +test_that("Commit message keyword search: any match, single string, only 2 commits", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, + strings = "add", + commit.hashes = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338")) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338"), + message = c("Add stuff ", + "Add some more stuff ")) + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message keyword search: any match, multiple strings", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(proj.conf) @@ -443,3 +484,18 @@ test_that("Commit message token counts", { ## Assert expect_equal(expected, result) }) + +test_that("Commit message token counts, omly 2 commits", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.message.counts(proj.data, c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338")) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338"), + count = c("2", "4")) + + ## Assert + expect_equal(expected, result) +}) From 4ead5b190aa85e4adffe8bc16597ce050e5bdafe Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 1 Apr 2025 14:33:15 +0200 Subject: [PATCH 36/92] Change 'README.md' incorporate requested changes Signed-off-by: Leo Sendelbach --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 84239187..04d5605b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ If you wonder: The name `coronet` derives as an acronym from the words "configur - [Core/Peripheral classification](#coreperipheral-classification) - [Count-based metrics](#count-based-metrics) - [Network-based metrics](#network-based-metrics) - - [Commit message functionalities](#commit-message-functionalities) + - [Commit-message functionalities](#commit-message-functionalities) - [How-to](#how-to) - [File/Module overview](#filemodule-overview) - [Configuration classes](#configuration-classes) @@ -148,6 +148,9 @@ Alternatively, you can run `Rscript install.R` to install the packages. - `Matrix`: For sparse matrix representation of large adjacency matrices (package version `1.3.0` or higher is required) - `fastmap`: For fast implementation of a map - `purrr`: For fast implementation of a mapping function +- `tm`: For NLP tasks used on commit messages +- `textstem`: For lemmatization of commit messages +- `SnowballC`: For text stemming, used by NLP package `tm` ### Submodule @@ -430,11 +433,9 @@ In this section, we provide descriptions of the different algorithms we provide * calculates scores based on the eccentricity of vertices in a network * eccentricity measures the length of the shortest path to each vertex's furthest reachable vertex -#### Commit message functionalities +#### Commit-message functionalities -In this section, we give an overview of the functionalities we offer regarding commit messages. These consist of basic NLP tasks, such as stemming (`get.stemmed.commit.messages`), tokenization (`get.tokenized.commit.messages`), and -lemmatization (`get.lemmatized.commit.messages`), as well as preprocessing steps (`get.preprocessed.commit.messages`) such as lowercase transformation and removal of punctuation, stopwords and extra whitespaces. Apart from these, -there is the option of using a set of strings to search for matching commits (`get.commit.messages.by.strings`) as well as getting token counts for commit messages (`get.commit.message.counts`). +In this section, we give an overview of the functionalities we offer regarding the textual analysis of commit messages. These consist of basic NLP tasks, such as stemming (`get.stemmed.commit.messages`), tokenization (`get.tokenized.commit.messages`), and lemmatization (`get.lmmatized.commit.messages`), as well as preprocessing steps (`get.preprocessed.commit.messages`) such as lowercase transformation and removal of punctuation, stopwords, and extra whitespaces. Apart from these, there is the option of searching for a set of strings in commit messages for matching commits (`get.commit.messages.by.strings`) as well as getting token counts for commit messages (`get.commit.message.counts`). ### How-to From ef689f71f248059cc69be4792ca14ce3b95dcac8 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 1 Apr 2025 14:36:06 +0200 Subject: [PATCH 37/92] Change loops to apply and do.call Incorporating PR feedback, updating documentation Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 6 +- util-data-misc.R | 156 +++++++++++++++++++++++------------------ 2 files changed, 89 insertions(+), 73 deletions(-) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 90deb6b1..1dc42fef 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -15,7 +15,7 @@ ## All Rights Reserved. -context("Tests for the file 'util-core-peripheral.R'") +context("Tests for the file 'util-data-misc.R'") ## ## Context @@ -479,7 +479,7 @@ test_that("Commit message token counts", { "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", "0a1a5c523d835459c42f33e863623138555e2526"), - count = c("2", "4", "8", "6", "2", "6", "0")) + count = c(2, 4, 8, 6, 2, 6, 0)) ## Assert expect_equal(expected, result) @@ -494,7 +494,7 @@ test_that("Commit message token counts, omly 2 commits", { ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338"), - count = c("2", "4")) + count = c(2, 4)) ## Assert expect_equal(expected, result) diff --git a/util-data-misc.R b/util-data-misc.R index eeedd990..79a11e70 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -30,7 +30,8 @@ requireNamespace("sqldf") # for SQL-selections on data.frames requireNamespace("logging") # for logging requireNamespace("tm") # for NLP functionalities -requireNamespace("SnowballC") # for stemming +requireNamespace("SnowballC") # for text stemming used by NLP package "tm" +requireNamespace("textstem") # for lemmatization #' Helper function to mask all issues in the issue data frame. #' @@ -773,19 +774,26 @@ get.issue.is.pull.request = function(proj.data) { } ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / -## Commit Message Functionalities ------------------------------------------ - -#' Apply preprocessing steps to commit messages of given commits -#' -#' @param proj.data the \code{ProjectData} containing the commit message data -#' @param commit.hashes the hashes of the commits that should be considered -#' @param preprocessing the preprocessing steps to be executed (all enabled by default) -#' -#' @return a dataframe containing the hashes and preprocesessed messages -get.preprocessed.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { +## Commit-Message Functionality ------------------------------------------ + +#' Apply preprocessing steps to commit messages of given commits. +#' +#' @param proj.data the \code{ProjectData} containing the commit-message data +#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered +#' [default: 'NULL'] +#' @param preprocessing the preprocessing steps to be executed +#' [default: c("lowercase", "punctuation", "stopwords", "whitespaces")] +#' +#' @return a dataframe containing the hashes and corresponding preprocesessed messages +get.preprocessed.commit.messages = function(proj.data, + commit.hashes = NULL, + preprocessing = c("lowercase", + "punctuation", + "stopwords", + "whitespaces")) { preprocessing = match.arg.or.default(preprocessing, several.ok = TRUE) preprocessed.messages = create.empty.data.frame(c("hash", "preprocessed.message")) - ## get commit message data of the given hashes + ## get commit-message data of the given hashes ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { @@ -798,14 +806,13 @@ get.preprocessed.commit.messages = function(proj.data, commit.hashes = NULL, pre } ## create a corpus with all selected commit messages - messages = c() - for (i in seq_len(nrow(commit.message.data))) { - current = commit.message.data[i, "title"] + messages = do.call(function(title, message, ...) { if (proj.data$get.project.conf.entry("commit.messages") == "message") { - current = paste(current, commit.message.data[i, "message"]) + return (paste(title, message)) + } else { + return (title) } - messages = c(messages, current) - } + }, commit.message.data) corpus = tm::Corpus(tm::VectorSource(messages)) ## preprocessing steps @@ -830,95 +837,103 @@ get.preprocessed.commit.messages = function(proj.data, commit.hashes = NULL, pre corpus = tm::tm_map(corpus, trimws) ## create output dataframe - for (i in seq_len(nrow(commit.message.data))) { - preprocessed.messages[i,] = c(commit.message.data[["hash"]][i], corpus$content[i]) - } + preprocessed.messages = data.frame(hash = commit.message.data[["hash"]], + preprocessed.message = corpus$content) return(preprocessed.messages) } #' Apply stemming to commit messages of given commits. Preprocessing will be executed as part of this. #' -#' @param proj.data the \code{ProjectData} containing the commit message data -#' @param commit.hashes the hashes of the commits that should be considered -#' @param preprocessing the preprocessing steps to be executed (all enabled by default) +#' @param proj.data the \code{ProjectData} containing the commit-message data +#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered +#' [default: 'NULL'] +#' @param preprocessing the preprocessing steps to be executed +#' [default: c("lowercase", "punctuation", "stopwords", "whitespaces")] #' -#' @return a dataframe containing the hashes and stemmed messages -get.stemmed.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { +#' @return a dataframe containing the hashes and corresponding stemmed messages +get.stemmed.commit.messages = function(proj.data, + commit.hashes = NULL, + preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { ## apply preprocessing preprocessed.messages = get.preprocessed.commit.messages(proj.data, commit.hashes, preprocessing) - stemmed.messages = create.empty.data.frame(c("hash", "stemmed.message")) ## build corpus - corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[,"preprocessed.message"])) + corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[, "preprocessed.message"])) ## apply stemming corpus = tm::tm_map(corpus, tm::stemDocument) ## create output dataframe - for (i in seq_len(nrow(preprocessed.messages))) { - stemmed.messages[i,] = c(preprocessed.messages[["hash"]][i], corpus$content[i]) - } + stemmed.messages = data.frame(hash = preprocessed.messages[["hash"]], + stemmed.message = corpus$content) return(stemmed.messages) } -#' Apply tokenization to commit messages of given commits. +#' Apply tokenization to commit messages of given commits. This function does not allow for preprocessing, +#' since it is supposed to extract all tokens from the text as is and preprocessing steps change the +#' resulting tokens. #' -#' @param proj.data the \code{ProjectData} containing the commit message data -#' @param commit.hashes the hashes of the commits that should be considered +#' @param proj.data the \code{ProjectData} containing the commit-message data +#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered +#' [default: 'NULL'] #' #' @return a list of vectors containing the tokens from the commit messages get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { - ## get commit message data of the given hashes + ## get commit-message data of the given hashes ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] } - tokens = list() - for (i in seq_len(nrow(commit.message.data))) { - current = commit.message.data[i, "title"] + + messages = do.call(function(title, message, ...) { if (proj.data$get.project.conf.entry("commit.messages") == "message") { - current = paste(current, commit.message.data[i, "message"]) + return (paste(title, message)) + } else { + return (title) } - ## add tokens to result - tokens[[length(tokens)+1]] = tm::Boost_tokenizer(current) - } + }, commit.message.data) + tokens = lapply(messages, tm::Boost_tokenizer) return(tokens) } #' Apply lemmatization to commit messages of given commits. Preprocessing will be executed as part of this. #' -#' @param proj.data the \code{ProjectData} containing the commit message data -#' @param commit.hashes the hashes of the commits that should be considered -#' @param preprocessing the preprocessing steps to be executed (all enabled by default) +#' @param proj.data the \code{ProjectData} containing the commit-message data +#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered +#' [default: 'NULL'] +#' @param preprocessing the preprocessing steps to be executed +#' [default: c("lowercase", "punctuation", "stopwords", "whitespaces")] #' -#' @return a dataframe containing the hashes and lemmatized messages -get.lemmatized.commit.messages = function(proj.data, commit.hashes = NULL, preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { +#' @return a dataframe containing the hashes and corresponding lemmatized messages +get.lemmatized.commit.messages = function(proj.data, + commit.hashes = NULL, + preprocessing = c("lowercase", "punctuation", "stopwords", "whitespaces")) { ## apply preprocessing preprocessed.messages = get.preprocessed.commit.messages(proj.data, commit.hashes, preprocessing) - lemmatized.messages = create.empty.data.frame(c("hash", "lemmatized.message")) ## build corpus corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[,"preprocessed.message"])) ## apply lemmatization corpus = tm::tm_map(corpus, textstem::lemmatize_strings) ## create output dataframe - for (i in seq_len(nrow(preprocessed.messages))) { - lemmatized.messages[i,] = c(preprocessed.messages[["hash"]][i], corpus$content[i]) - } + lemmatized.messages = data.frame(hash = preprocessed.messages[["hash"]], + lemmatized.message = corpus$content) return(lemmatized.messages) } -#' Get Commits messages that match given strings. +#' Get commit messages that match given strings. #' -#' @param proj.data the \code{ProjectData} containing the commit message data -#' @param commit.hashes the hashes of the commits that should be considered +#' @param proj.data the \code{ProjectData} containing the commit-message data +#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered +#' [default: 'NULL'] #' @param strings the strings that are searched for -#' @param match the method which describes how many of the strings need to be in a message in order for -#' that message to be returned (default: any) +#' @param match the function that describes how many of the strings need to be in a message in order for +#' that message to be returned. Can be any function that takes a list of logical values and +#' returns a single logical value, such as 'any', 'all' or anything in between [default: any] #' -#' @return a dataframe containing the hashes and matching messages +#' @return a dataframe containing the hashes and corresponding matching messages get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strings, match = any) { messages = create.empty.data.frame(c("hash", "message")) - ## get commit message data of the given hashes + ## get commit-message data of the given hashes ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { @@ -926,16 +941,18 @@ get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strin } for (i in seq_len(nrow(commit.message.data))) { + ## get the title of the message, as it is should always be present current = commit.message.data[i, "title"] + ## if config parameter is set to 'message', also append the message body if (proj.data$get.project.conf.entry("commit.messages") == "message") { current = paste(current, commit.message.data[i, "message"]) } - ## check if message contains strings + ## check if message contains 'strings' check = lapply(strings, function(word) { return (grepl(word, current, ignore.case = TRUE)) }) if (match(check)) { - messages[nrow(messages)+1,] = c(commit.message.data[["hash"]][i], current) + messages[nrow(messages) + 1, ] = c(commit.message.data[["hash"]][i], current) } } return(messages) @@ -944,13 +961,13 @@ get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strin #' Count tokens in given commit messages. #' -#' @param proj.data the \code{ProjectData} containing the commit message data -#' @param commit.hashes the hashes of the commits that should be considered +#' @param proj.data the \code{ProjectData} containing the commit-message data +#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered +#' [default: 'NULL'] #' -#' @return a dataframe containing the hashes and token counts +#' @return a dataframe containing the hashes and corresponding token counts get.commit.message.counts = function(proj.data, commit.hashes = NULL) { - messages = create.empty.data.frame(c("hash", "count")) - ## get commit message data of the given hashes + ## get commit-message data of the given hashes ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { @@ -959,10 +976,9 @@ get.commit.message.counts = function(proj.data, commit.hashes = NULL) { ## get tokens tokens = get.tokenized.commit.messages(proj.data, commit.hashes) - for (i in seq_len(nrow(commit.message.data))) { - hash = commit.message.data[["hash"]][i] - ## count tokens - messages[nrow(messages)+1,] = c(hash, length(tokens[[i]])) - } + hashes = commit.message.data[["hash"]] + counts = unlist(lapply(tokens, length)) + messages = data.frame(hash = hashes, + count = counts) return(messages) } From 6e642242a3063663bcc3c7f5cca0650dfebb6bb4 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Mon, 7 Apr 2025 13:43:07 +0200 Subject: [PATCH 38/92] Remove do.call and loops all 'do.call' and 'for' occurences have been replaced with 'apply' and/or 'paste' on vectors Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 5 +++-- util-data-misc.R | 49 ++++++++++++++++++------------------------ 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 1dc42fef..0a4eab33 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -402,6 +402,7 @@ test_that("Commit message keyword search: any match, multiple strings", { "Add some more stuff ", "I added important things the things are\nnothing", "Wish intensifies")) + rownames(result) = NULL ## Assert expect_equal(expected, result) }) @@ -409,7 +410,7 @@ test_that("Commit message keyword search: any match, multiple strings", { test_that("Commit message keyword search: any match, multiple strings, only title", { proj.conf$update.value("commit.messages", "title") proj.data = ProjectData$new(proj.conf) - result = get.commit.messages.by.strings(proj.data, strings = c("add", "intensifies")) + result = get.commit.messages.by.strings(proj.data, strings = c("add", "intensifies")) ## Act expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", @@ -428,7 +429,7 @@ test_that("Commit message keyword search: all match, multiple strings, no result result = get.commit.messages.by.strings(proj.data, strings = c("add", "intensifies"), match = all) ## Act - expected = create.empty.data.frame(c("hash", "message")) + expected = create.empty.data.frame(c("hash", "message"), c("character", "character")) ## Assert expect_equal(expected, result) }) diff --git a/util-data-misc.R b/util-data-misc.R index 79a11e70..cb7babd3 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -806,13 +806,10 @@ get.preprocessed.commit.messages = function(proj.data, } ## create a corpus with all selected commit messages - messages = do.call(function(title, message, ...) { - if (proj.data$get.project.conf.entry("commit.messages") == "message") { - return (paste(title, message)) - } else { - return (title) - } - }, commit.message.data) + messages = commit.message.data$title + if (proj.data$get.project.conf.entry("commit.messages") == "message") { + messages = paste(messages, commit.message.data$message) + } corpus = tm::Corpus(tm::VectorSource(messages)) ## preprocessing steps @@ -884,13 +881,10 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] } - messages = do.call(function(title, message, ...) { - if (proj.data$get.project.conf.entry("commit.messages") == "message") { - return (paste(title, message)) - } else { - return (title) - } - }, commit.message.data) + messages = commit.message.data$title + if (proj.data$get.project.conf.entry("commit.messages") == "message") { + messages = paste(messages, commit.message.data$message) + } tokens = lapply(messages, tm::Boost_tokenizer) return(tokens) @@ -940,21 +934,20 @@ get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strin commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] } - for (i in seq_len(nrow(commit.message.data))) { - ## get the title of the message, as it is should always be present - current = commit.message.data[i, "title"] - ## if config parameter is set to 'message', also append the message body - if (proj.data$get.project.conf.entry("commit.messages") == "message") { - current = paste(current, commit.message.data[i, "message"]) - } - ## check if message contains 'strings' - check = lapply(strings, function(word) { - return (grepl(word, current, ignore.case = TRUE)) - }) - if (match(check)) { - messages[nrow(messages) + 1, ] = c(commit.message.data[["hash"]][i], current) - } + ## prepare the dataframe for searching commit messages by merging the 'title' and 'message' columns if desired + if (proj.data$get.project.conf.entry("commit.messages") == "message") { + commit.message.data$message = paste(commit.message.data[["title"]], commit.message.data[["message"]]) + } else { + commit.message.data$message = commit.message.data[["title"]] } + commit.message.data = commit.message.data[c("hash", "message")] + ## filter the messages by searching for all the keywords in 'strings' + ## and applying the match function once per message + messages = commit.message.data[unlist(lapply(commit.message.data$message, function(msg) { + match(lapply(strings, function(word) { + return (grepl(word, msg, ignore.case = TRUE)) + })) + })), ] return(messages) } From 8df6b8828fab567fe529baffa7c6278a3c690863 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Mon, 7 Apr 2025 13:58:40 +0200 Subject: [PATCH 39/92] Add new functionality to 'showcase.R' also rename the chapter in 'README.md' Signed-off-by: Leo Sendelbach --- README.md | 4 ++-- showcase.R | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 04d5605b..d0617092 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ If you wonder: The name `coronet` derives as an acronym from the words "configur - [Core/Peripheral classification](#coreperipheral-classification) - [Count-based metrics](#count-based-metrics) - [Network-based metrics](#network-based-metrics) - - [Commit-message functionalities](#commit-message-functionalities) + - [Commit-message content analysis](#commit-message-content-analysis) - [How-to](#how-to) - [File/Module overview](#filemodule-overview) - [Configuration classes](#configuration-classes) @@ -433,7 +433,7 @@ In this section, we provide descriptions of the different algorithms we provide * calculates scores based on the eccentricity of vertices in a network * eccentricity measures the length of the shortest path to each vertex's furthest reachable vertex -#### Commit-message functionalities +#### Commit-message content analysis In this section, we give an overview of the functionalities we offer regarding the textual analysis of commit messages. These consist of basic NLP tasks, such as stemming (`get.stemmed.commit.messages`), tokenization (`get.tokenized.commit.messages`), and lemmatization (`get.lmmatized.commit.messages`), as well as preprocessing steps (`get.preprocessed.commit.messages`) such as lowercase transformation and removal of punctuation, stopwords, and extra whitespaces. Apart from these, there is the option of searching for a set of strings in commit messages for matching commits (`get.commit.messages.by.strings`) as well as getting token counts for commit messages (`get.commit.message.counts`). diff --git a/showcase.R b/showcase.R index 9ab5934a..9a9f44a9 100644 --- a/showcase.R +++ b/showcase.R @@ -24,7 +24,7 @@ ## Copyright 2021 by Niklas Schneider ## Copyright 2022 by Jonathan Baumann ## Copyright 2024 by Maximilian Löffler -## Copyright 2024 by Leo Sendelbach +## Copyright 2024-2025 by Leo Sendelbach ## All Rights Reserved. @@ -501,3 +501,13 @@ calculate.cohens.kappa(author.classification.list = author.class.overview, get.class.turnover.overview(author.class.overview = author.class.overview) get.unstable.authors.overview(author.class.overview = author.class.overview, saturation = 2) +## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / +## Commit-message content analysis ----------------------------------------- + +get.stemmed.commit.messages(y.data) + +get.tokenized.commit.messages(y.data) + +get.lemmatized.commit.messages(y.data, + commit.hashes = c("3a0ed78458b3976243db6829f63eba3eead26774"), + preprocessing = ('stopwords')) From fe08cf7bb5f4b42e6aa44ec93caa3b06e81a9380 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Mon, 7 Apr 2025 14:12:45 +0200 Subject: [PATCH 40/92] Update 'NEWS.md' includes updated hashes after rebasing onto previous merged PR Signed-off-by: Leo Sendelbach --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 0a7f41a9..8abf8af9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,6 +7,7 @@ ### Added - Add the possibility to split networks that contain simplified edges (PR #278, 9798d33512dcdf50d3b88a1223fc4913a2a88eeb, 0ed437c14423c1917f1ba470e7e55db4626d380b, 67a6651b94d50cb7c2ab4a74888b0556d607b102, 98ef83158204be2a67b115cb25df5ba375cccf60, 7ec4d83fdeb308a24a350acd808941807b9511f1, 637d62ab70f098f26f241e588a99cdc49d10f56a, 2c70666f128f96a3a573f29a0cbbef14d803d193, 1cbc6fa36859d6db3a7ff4493ef19763e87d2de3, 41788ff029d038969bfc6b5773e919201c5ac595, b042c0dd08e2229514ccd25dee7a119f25b1ab45, 36d23d657f412aa1953c4773076e593273f19d8e, 402c256d9a05e4ffb297d4ea1fc25d0230787bc0, 54af2b19a112070f10d191b98b055482748426a7, 894414a4a970822b9ecd59c0b6c480860707f636, 0fe32a259ef703c2de79135bfa6932a595fdc1c5) +- Add functionality for commit-message content analysis, such as NLP tools including stemming, tokenization, and lemmatization, as well as a function to search for keywords in commit messages and a function to measure the length of the messages (PR #281, 5aa4e4193f0c00095fedf961c6060a5c035ef9c6, 99f0638566c0062b987617bc3fe3ace1db7729ee, e469d3a0cf2881c378469b6ccfea9c204d13f19b, 7d8fd39f164c776921e3fb36daf79256e7be7426, ef689f71f248059cc69be4792ca14ce3b95dcac8, 6e642242a3063663bcc3c7f5cca0650dfebb6bb4) ### Changed/Improved From f54439486115cada08dac23864b2f7605edca9ea Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 8 Apr 2025 16:22:10 +0200 Subject: [PATCH 41/92] Add parallelization and if-else constructs incorporate PR feedback, update showcase and comments Signed-off-by: Leo Sendelbach --- showcase.R | 12 ++++++-- tests/test-data-misc.R | 6 ++++ util-data-misc.R | 62 ++++++++++++++++++++++++------------------ 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/showcase.R b/showcase.R index 9a9f44a9..33221f09 100644 --- a/showcase.R +++ b/showcase.R @@ -504,10 +504,16 @@ get.unstable.authors.overview(author.class.overview = author.class.overview, sat ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / ## Commit-message content analysis ----------------------------------------- -get.stemmed.commit.messages(y.data) - +## analyze all commit messages using tokenization, which does not employ any preprocessing steps get.tokenized.commit.messages(y.data) +## analyze the commit message of a specific commit with only one preprocessing step get.lemmatized.commit.messages(y.data, commit.hashes = c("3a0ed78458b3976243db6829f63eba3eead26774"), - preprocessing = ('stopwords')) + preprocessing = "stopwords") + +## analyze all commits using default preprocessing +get.preprocessed.commit.messages(y.data) + +## analyze all commit messages with two preprocessing steps +get.stemmed.commit.messages(y.data, preprocessing = c("whitespaces", "lowercase")) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 0a4eab33..8e3032c8 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -367,6 +367,7 @@ test_that("Commit message keyword search: any match, single string", { message = c("Add stuff ", "Add some more stuff ", "I added important things the things are\nnothing")) + rownames(result) = NULL ## Assert expect_equal(expected, result) }) @@ -384,6 +385,7 @@ test_that("Commit message keyword search: any match, single string, only 2 commi "5a5ec9675e98187e1e92561e1888aa6f04faa338"), message = c("Add stuff ", "Add some more stuff ")) + rownames(result) = NULL ## Assert expect_equal(expected, result) }) @@ -419,6 +421,7 @@ test_that("Commit message keyword search: any match, multiple strings, only titl message = c("Add stuff", "Add some more stuff", "I added important things")) + rownames(result) = NULL ## Assert expect_equal(expected, result) }) @@ -430,6 +433,7 @@ test_that("Commit message keyword search: all match, multiple strings, no result ## Act expected = create.empty.data.frame(c("hash", "message"), c("character", "character")) + rownames(result) = NULL ## Assert expect_equal(expected, result) }) @@ -444,6 +448,7 @@ test_that("Commit message keyword search: all match, multiple strings", { "5a5ec9675e98187e1e92561e1888aa6f04faa338"), message = c("Add stuff ", "Add some more stuff ")) + rownames(result) = NULL ## Assert expect_equal(expected, result) }) @@ -463,6 +468,7 @@ test_that("Commit message keyword search: at least 2 match, multiple strings", { message = c("Add stuff ", "Add some more stuff ", "I added important things the things are\nnothing")) + rownames(result) = NULL ## Assert expect_equal(expected, result) }) diff --git a/util-data-misc.R b/util-data-misc.R index cb7babd3..2c379785 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -32,6 +32,7 @@ requireNamespace("logging") # for logging requireNamespace("tm") # for NLP functionalities requireNamespace("SnowballC") # for text stemming used by NLP package "tm" requireNamespace("textstem") # for lemmatization +requireNamespace("parallel") # for parallelization of commit-message keyword search #' Helper function to mask all issues in the issue data frame. #' @@ -776,11 +777,12 @@ get.issue.is.pull.request = function(proj.data) { ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / ## Commit-Message Functionality ------------------------------------------ -#' Apply preprocessing steps to commit messages of given commits. +#' Apply preprocessing steps to commit messages of given commits. Preprocessing steps are always performed in +#' the following order: \code{lowercase} -> \code{punctuation} -> \code{stopwords} -> \code{whitespaces} #' #' @param proj.data the \code{ProjectData} containing the commit-message data -#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered -#' [default: 'NULL'] +#' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered +#' [default: NULL] #' @param preprocessing the preprocessing steps to be executed #' [default: c("lowercase", "punctuation", "stopwords", "whitespaces")] #' @@ -797,7 +799,7 @@ get.preprocessed.commit.messages = function(proj.data, ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { - commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + commit.message.data = commit.message.data[commit.message.data[["hash"]] %in% commit.hashes, ] } ## if data is empty, abort process @@ -806,9 +808,11 @@ get.preprocessed.commit.messages = function(proj.data, } ## create a corpus with all selected commit messages - messages = commit.message.data$title + messages = c() if (proj.data$get.project.conf.entry("commit.messages") == "message") { - messages = paste(messages, commit.message.data$message) + messages = paste(commit.message.data[["title"]], commit.message.data[["message"]]) + } else { + messages = commit.message.data[["title"]] } corpus = tm::Corpus(tm::VectorSource(messages)) @@ -841,10 +845,12 @@ get.preprocessed.commit.messages = function(proj.data, } #' Apply stemming to commit messages of given commits. Preprocessing will be executed as part of this. +#' Preprocessing steps are always performed in the following order: +#' \code{lowercase} -> \code{punctuation} -> \code{stopwords} -> \code{whitespaces} #' #' @param proj.data the \code{ProjectData} containing the commit-message data -#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered -#' [default: 'NULL'] +#' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered +#' [default: NULL] #' @param preprocessing the preprocessing steps to be executed #' [default: c("lowercase", "punctuation", "stopwords", "whitespaces")] #' @@ -869,8 +875,8 @@ get.stemmed.commit.messages = function(proj.data, #' resulting tokens. #' #' @param proj.data the \code{ProjectData} containing the commit-message data -#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered -#' [default: 'NULL'] +#' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered +#' [default: NULL] #' #' @return a list of vectors containing the tokens from the commit messages get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { @@ -878,12 +884,14 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { - commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + commit.message.data = commit.message.data[commit.message.data[["hash"]] %in% commit.hashes, ] } - messages = commit.message.data$title + messages = c() if (proj.data$get.project.conf.entry("commit.messages") == "message") { - messages = paste(messages, commit.message.data$message) + messages = paste(commit.message.data[["title"]], commit.message.data[["message"]]) + } else { + messages = commit.message.data[["title"]] } tokens = lapply(messages, tm::Boost_tokenizer) @@ -891,10 +899,12 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { } #' Apply lemmatization to commit messages of given commits. Preprocessing will be executed as part of this. +#' Preprocessing steps are always performed in the following order: +#' \code{lowercase} -> \code{punctuation} -> \code{stopwords} -> \code{whitespaces} #' #' @param proj.data the \code{ProjectData} containing the commit-message data -#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered -#' [default: 'NULL'] +#' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered +#' [default: NULL] #' @param preprocessing the preprocessing steps to be executed #' [default: c("lowercase", "punctuation", "stopwords", "whitespaces")] #' @@ -917,12 +927,12 @@ get.lemmatized.commit.messages = function(proj.data, #' Get commit messages that match given strings. #' #' @param proj.data the \code{ProjectData} containing the commit-message data -#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered -#' [default: 'NULL'] +#' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered +#' [default: NULL] #' @param strings the strings that are searched for #' @param match the function that describes how many of the strings need to be in a message in order for #' that message to be returned. Can be any function that takes a list of logical values and -#' returns a single logical value, such as 'any', 'all' or anything in between [default: any] +#' returns a single logical value, such as \code{any}, \code{all} or anything in between [default: any] #' #' @return a dataframe containing the hashes and corresponding matching messages get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strings, match = any) { @@ -931,19 +941,19 @@ get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strin ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { - commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + commit.message.data = commit.message.data[commit.message.data[["hash"]] %in% commit.hashes, ] } ## prepare the dataframe for searching commit messages by merging the 'title' and 'message' columns if desired if (proj.data$get.project.conf.entry("commit.messages") == "message") { - commit.message.data$message = paste(commit.message.data[["title"]], commit.message.data[["message"]]) + commit.message.data[["message"]] = paste(commit.message.data[["title"]], commit.message.data[["message"]]) } else { - commit.message.data$message = commit.message.data[["title"]] + commit.message.data[["message"]] = commit.message.data[["title"]] } - commit.message.data = commit.message.data[c("hash", "message")] + commit.message.data = commit.message.data[, c("hash", "message")] ## filter the messages by searching for all the keywords in 'strings' ## and applying the match function once per message - messages = commit.message.data[unlist(lapply(commit.message.data$message, function(msg) { + messages = commit.message.data[unlist(parallel::mclapply(commit.message.data[["message"]], function(msg) { match(lapply(strings, function(word) { return (grepl(word, msg, ignore.case = TRUE)) })) @@ -955,8 +965,8 @@ get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strin #' Count tokens in given commit messages. #' #' @param proj.data the \code{ProjectData} containing the commit-message data -#' @param commit.hashes the commit hashes that should be considered, if 'NULL' all commits are considered -#' [default: 'NULL'] +#' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered +#' [default: NULL] #' #' @return a dataframe containing the hashes and corresponding token counts get.commit.message.counts = function(proj.data, commit.hashes = NULL) { @@ -964,7 +974,7 @@ get.commit.message.counts = function(proj.data, commit.hashes = NULL) { ## if no hashes are given consider all commits commit.message.data = proj.data$get.commit.messages() if (!is.null(commit.hashes)) { - commit.message.data = commit.message.data[commit.message.data$hash %in% commit.hashes, ] + commit.message.data = commit.message.data[commit.message.data[["hash"]] %in% commit.hashes, ] } ## get tokens tokens = get.tokenized.commit.messages(proj.data, commit.hashes) From 505b7fcfe8f02dd85611b83d50676e4193c6e4d6 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 15 Apr 2025 14:28:17 +0200 Subject: [PATCH 42/92] Incorporate requested changes change comments and remove unnecessary declarations Signed-off-by: Leo Sendelbach --- util-data-misc.R | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/util-data-misc.R b/util-data-misc.R index 2c379785..a9cea060 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -32,7 +32,7 @@ requireNamespace("logging") # for logging requireNamespace("tm") # for NLP functionalities requireNamespace("SnowballC") # for text stemming used by NLP package "tm" requireNamespace("textstem") # for lemmatization -requireNamespace("parallel") # for parallelization of commit-message keyword search +requireNamespace("parallel") # for for parallel computation #' Helper function to mask all issues in the issue data frame. #' @@ -778,7 +778,7 @@ get.issue.is.pull.request = function(proj.data) { ## Commit-Message Functionality ------------------------------------------ #' Apply preprocessing steps to commit messages of given commits. Preprocessing steps are always performed in -#' the following order: \code{lowercase} -> \code{punctuation} -> \code{stopwords} -> \code{whitespaces} +#' the following order: \code{'lowercase'} -> \code{'punctuation'} -> \code{'stopwords'} -> \code{'whitespaces'} #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -808,7 +808,6 @@ get.preprocessed.commit.messages = function(proj.data, } ## create a corpus with all selected commit messages - messages = c() if (proj.data$get.project.conf.entry("commit.messages") == "message") { messages = paste(commit.message.data[["title"]], commit.message.data[["message"]]) } else { @@ -846,7 +845,7 @@ get.preprocessed.commit.messages = function(proj.data, #' Apply stemming to commit messages of given commits. Preprocessing will be executed as part of this. #' Preprocessing steps are always performed in the following order: -#' \code{lowercase} -> \code{punctuation} -> \code{stopwords} -> \code{whitespaces} +#' \code{'lowercase'} -> \code{'punctuation'} -> \code{'stopwords'} -> \code{'whitespaces'} #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -887,7 +886,7 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { commit.message.data = commit.message.data[commit.message.data[["hash"]] %in% commit.hashes, ] } - messages = c() + if (proj.data$get.project.conf.entry("commit.messages") == "message") { messages = paste(commit.message.data[["title"]], commit.message.data[["message"]]) } else { @@ -900,7 +899,7 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { #' Apply lemmatization to commit messages of given commits. Preprocessing will be executed as part of this. #' Preprocessing steps are always performed in the following order: -#' \code{lowercase} -> \code{punctuation} -> \code{stopwords} -> \code{whitespaces} +#' \code{'lowercase'} -> \code{'punctuation'} -> \code{'stopwords'} -> \code{'whitespaces'} #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -915,7 +914,7 @@ get.lemmatized.commit.messages = function(proj.data, ## apply preprocessing preprocessed.messages = get.preprocessed.commit.messages(proj.data, commit.hashes, preprocessing) ## build corpus - corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[,"preprocessed.message"])) + corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[, "preprocessed.message"])) ## apply lemmatization corpus = tm::tm_map(corpus, textstem::lemmatize_strings) ## create output dataframe From dd9246b2f4506d3d58f1c1f37fc198aaaafebb0d Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Wed, 14 May 2025 15:40:24 +0200 Subject: [PATCH 43/92] Change preprocessing order also improve comments and give option to make keyword search case sensitive Signed-off-by: Leo Sendelbach --- tests/test-data-misc.R | 23 +++++++++++++++++++---- util-data-misc.R | 35 +++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/tests/test-data-misc.R b/tests/test-data-misc.R index 8e3032c8..f490a185 100644 --- a/tests/test-data-misc.R +++ b/tests/test-data-misc.R @@ -173,7 +173,7 @@ test_that("Commit message preprocessing steps: All preprocesing", { "added important things things nothing", "wish work now", "wish intensifies", - "still doesnt work expected", + "still work expected", "")) ## Assert expect_equal(expected, result) @@ -205,7 +205,7 @@ test_that("Commit message preprocessing steps: limited commit number", { "0a1a5c523d835459c42f33e863623138555e2526"), preprocessed.message = c("add stuff", "add stuff", - "still doesnt work expected", + "still work expected", "")) ## Assert expect_equal(expected, result) @@ -253,7 +253,7 @@ test_that("Commit message stemming: All preprocesing", { "ad import thing thing noth", "wish work now", "wish intensifi", - "still doesnt work expect", + "still work expect", "")) ## Assert expect_equal(expected, result) @@ -349,7 +349,7 @@ test_that("Commit message lemmatization: All preprocesing", { "add important thing thing nothing", "wish work now", "wish intensify", - "still doesnt work expect", + "still work expect", "")) ## Assert expect_equal(expected, result) @@ -409,6 +409,21 @@ test_that("Commit message keyword search: any match, multiple strings", { expect_equal(expected, result) }) +test_that("Commit message keyword search: any match, multiple strings, case sensitive", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.commit.messages.by.strings(proj.data, strings = c("Add", "Intensifies"), ignore.case = FALSE) + + ## Act + expected = data.frame(hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", + "5a5ec9675e98187e1e92561e1888aa6f04faa338"), + message = c("Add stuff ", + "Add some more stuff ")) + rownames(result) = NULL + ## Assert + expect_equal(expected, result) +}) + test_that("Commit message keyword search: any match, multiple strings, only title", { proj.conf$update.value("commit.messages", "title") proj.data = ProjectData$new(proj.conf) diff --git a/util-data-misc.R b/util-data-misc.R index a9cea060..15518e97 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -778,7 +778,18 @@ get.issue.is.pull.request = function(proj.data) { ## Commit-Message Functionality ------------------------------------------ #' Apply preprocessing steps to commit messages of given commits. Preprocessing steps are always performed in -#' the following order: \code{'lowercase'} -> \code{'punctuation'} -> \code{'stopwords'} -> \code{'whitespaces'} +#' the following order: \code{'lowercase'} -> \code{'stopwords'} -> \code{'punctuation'} -> \code{'whitespaces'} +#' +#' \code{'lowercase'} transforms all upper case characters into their lowercase counterparts +#' \code{'stopwords'} removes all stopwords using a list of stopwords +#' for the english language provided by the package 'tm' +#' \code{'punctuaton'} removes all punctuation, as described in the ASCII \code{[:punct:]} class, +#' using the r-base \code{regex} functionality. This includes standard punctuation +#' characters such as ',', '.', ':', etc. but also dashes, parantheses, mathematical +#' symbols and special characters used in programming, such as '$', '#', or '&'. +#' Intra-word dashes are kept. +#' \code{'whitespaces'} removes superflous whitespace characters, such as '\t' or '\n', and replaces +#' them with single whitespaces #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -820,14 +831,14 @@ get.preprocessed.commit.messages = function(proj.data, ## convert to lowercase corpus = tm::tm_map(corpus, tm::content_transformer(tolower)) } - if ("punctuation" %in% preprocessing) { - ## remove punctuation - corpus = tm::tm_map(corpus, tm::removePunctuation) - } if ("stopwords" %in% preprocessing) { ## remove stopwords corpus = tm::tm_map(corpus, tm::removeWords, tm::stopwords("english")) } + if ("punctuation" %in% preprocessing) { + ## remove punctuation + corpus = tm::tm_map(corpus, tm::removePunctuation, preserve_intra_word_dashes = TRUE) + } if ("whitespaces" %in% preprocessing) { ## remove excess whitespaces corpus = tm::tm_map(corpus, tm::stripWhitespace) @@ -845,7 +856,7 @@ get.preprocessed.commit.messages = function(proj.data, #' Apply stemming to commit messages of given commits. Preprocessing will be executed as part of this. #' Preprocessing steps are always performed in the following order: -#' \code{'lowercase'} -> \code{'punctuation'} -> \code{'stopwords'} -> \code{'whitespaces'} +#' \code{'lowercase'} -> \code{'stopwords'} -> \code{'punctuation'} -> \code{'whitespaces'} #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -871,7 +882,8 @@ get.stemmed.commit.messages = function(proj.data, #' Apply tokenization to commit messages of given commits. This function does not allow for preprocessing, #' since it is supposed to extract all tokens from the text as is and preprocessing steps change the -#' resulting tokens. +#' resulting tokens. The text is split into tokens at any whitespace character. +#' Special characters have no impact and are treated the same as any other non-whitespace character. #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -885,8 +897,6 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { if (!is.null(commit.hashes)) { commit.message.data = commit.message.data[commit.message.data[["hash"]] %in% commit.hashes, ] } - - if (proj.data$get.project.conf.entry("commit.messages") == "message") { messages = paste(commit.message.data[["title"]], commit.message.data[["message"]]) } else { @@ -899,7 +909,7 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { #' Apply lemmatization to commit messages of given commits. Preprocessing will be executed as part of this. #' Preprocessing steps are always performed in the following order: -#' \code{'lowercase'} -> \code{'punctuation'} -> \code{'stopwords'} -> \code{'whitespaces'} +#' \code{'lowercase'} -> \code{'stopwords'} -> \code{'punctuation'} -> \code{'whitespaces'} #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -932,9 +942,10 @@ get.lemmatized.commit.messages = function(proj.data, #' @param match the function that describes how many of the strings need to be in a message in order for #' that message to be returned. Can be any function that takes a list of logical values and #' returns a single logical value, such as \code{any}, \code{all} or anything in between [default: any] +#' @param ignore.case whether the case should be ignored in the search [default: TRUE] #' #' @return a dataframe containing the hashes and corresponding matching messages -get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strings, match = any) { +get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strings, match = any, ignore.case = TRUE) { messages = create.empty.data.frame(c("hash", "message")) ## get commit-message data of the given hashes ## if no hashes are given consider all commits @@ -954,7 +965,7 @@ get.commit.messages.by.strings = function(proj.data, commit.hashes = NULL, strin ## and applying the match function once per message messages = commit.message.data[unlist(parallel::mclapply(commit.message.data[["message"]], function(msg) { match(lapply(strings, function(word) { - return (grepl(word, msg, ignore.case = TRUE)) + return (grepl(word, msg, ignore.case = ignore.case)) })) })), ] return(messages) From 1f3772f3c2e874212cc0431b0644d4cf42e81878 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Thu, 22 May 2025 14:44:56 +0200 Subject: [PATCH 44/92] Update comments to use " instead of ' Using " in function documentation, while using ' in in-line comments Signed-off-by: Leo Sendelbach --- util-data-misc.R | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/util-data-misc.R b/util-data-misc.R index 15518e97..94a6c2b1 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -778,17 +778,17 @@ get.issue.is.pull.request = function(proj.data) { ## Commit-Message Functionality ------------------------------------------ #' Apply preprocessing steps to commit messages of given commits. Preprocessing steps are always performed in -#' the following order: \code{'lowercase'} -> \code{'stopwords'} -> \code{'punctuation'} -> \code{'whitespaces'} +#' the following order: \code{"lowercase"} -> \code{"stopwords"} -> \code{"punctuation"} -> \code{"whitespaces"} #' -#' \code{'lowercase'} transforms all upper case characters into their lowercase counterparts -#' \code{'stopwords'} removes all stopwords using a list of stopwords -#' for the english language provided by the package 'tm' -#' \code{'punctuaton'} removes all punctuation, as described in the ASCII \code{[:punct:]} class, +#' \code{"lowercase"} transforms all upper case characters into their lowercase counterparts +#' \code{"stopwords"} removes all stopwords using a list of stopwords +#' for the english language provided by the package \code{tm} +#' \code{"punctuaton"} removes all punctuation, as described in the ASCII \code{[:punct:]} class, #' using the r-base \code{regex} functionality. This includes standard punctuation -#' characters such as ',', '.', ':', etc. but also dashes, parantheses, mathematical -#' symbols and special characters used in programming, such as '$', '#', or '&'. +#' characters such as ",", ".", ":", etc. but also dashes, parantheses, mathematical +#' symbols and special characters used in programming, such as "$", "#", or "&". #' Intra-word dashes are kept. -#' \code{'whitespaces'} removes superflous whitespace characters, such as '\t' or '\n', and replaces +#' \code{"whitespaces"} removes superflous whitespace characters, such as "\t" or "\n", and replaces #' them with single whitespaces #' #' @param proj.data the \code{ProjectData} containing the commit-message data @@ -856,7 +856,7 @@ get.preprocessed.commit.messages = function(proj.data, #' Apply stemming to commit messages of given commits. Preprocessing will be executed as part of this. #' Preprocessing steps are always performed in the following order: -#' \code{'lowercase'} -> \code{'stopwords'} -> \code{'punctuation'} -> \code{'whitespaces'} +#' \code{"lowercase"} -> \code{"stopwords"} -> \code{"punctuation"} -> \code{"whitespaces"} #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered @@ -909,7 +909,7 @@ get.tokenized.commit.messages = function(proj.data, commit.hashes = NULL) { #' Apply lemmatization to commit messages of given commits. Preprocessing will be executed as part of this. #' Preprocessing steps are always performed in the following order: -#' \code{'lowercase'} -> \code{'stopwords'} -> \code{'punctuation'} -> \code{'whitespaces'} +#' \code{"lowercase"} -> \code{"stopwords"} -> \code{"punctuation"} -> \code{"whitespaces"} #' #' @param proj.data the \code{ProjectData} containing the commit-message data #' @param commit.hashes the commit hashes that should be considered, if \code{NULL} all commits are considered From 3dc91b155b3e0e2a55378592db448606381f902e Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 27 May 2025 16:01:41 +0200 Subject: [PATCH 45/92] Deprecate support for R4.0 Removed R4.0 from workflow and changed minimum R version in README.md Signed-off-by: Leo Sendelbach --- .github/workflows/pull_request.yml | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 7be58b2a..503de3d0 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -13,6 +13,7 @@ ## ## Copyright 2023-2024 by Maximilian Löffler ## Copyright 2024 by Thomas Bock +## Copyright 2025 by Leo Sendelbach ## All Rights Reserved. name: Build Status @@ -37,7 +38,7 @@ jobs: strategy: fail-fast: false matrix: - r-version: ['4.0', '4.1', '4.2', '4.3', '4.4', 'latest'] + r-version: ['4.1', '4.2', '4.3', '4.4', '4.5', 'latest'] steps: - name: Checkout Repo diff --git a/README.md b/README.md index d0617092..b19ec246 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ While using the package, we require the following infrastructure. #### [`R`](https://www.r-project.org/) -Minimum requirement is `R` version `4.0.5`. Hence, later `R` versions also work. (Earlier `R` versions beginning from version `3.3.1` on should also work, but some packages are not available any more for these versions, so we do not test them any more in our CI pipeline.) +Minimum requirement is `R` version `4.1.1`. Hence, later `R` versions also work. (Earlier `R` versions beginning from version `3.3.1` on should also work, but some packages are not available any more for these versions, so we do not test them any more in our CI pipeline.) We currently *recommend* `R` version `4.1.1` or `4.3.0` for reliability reasons and `packrat` compatibility, but also later `R` versions should work (and are tested using our CI script). From 5aa3ca9732bc9ad49342d54eb71bbda91761a9e3 Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 27 May 2025 16:08:56 +0200 Subject: [PATCH 46/92] Update NEWS.md Add news about R version changes and add previous commits Signed-off-by: Leo Sendelbach --- NEWS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 8abf8af9..f3eb2459 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,7 +7,8 @@ ### Added - Add the possibility to split networks that contain simplified edges (PR #278, 9798d33512dcdf50d3b88a1223fc4913a2a88eeb, 0ed437c14423c1917f1ba470e7e55db4626d380b, 67a6651b94d50cb7c2ab4a74888b0556d607b102, 98ef83158204be2a67b115cb25df5ba375cccf60, 7ec4d83fdeb308a24a350acd808941807b9511f1, 637d62ab70f098f26f241e588a99cdc49d10f56a, 2c70666f128f96a3a573f29a0cbbef14d803d193, 1cbc6fa36859d6db3a7ff4493ef19763e87d2de3, 41788ff029d038969bfc6b5773e919201c5ac595, b042c0dd08e2229514ccd25dee7a119f25b1ab45, 36d23d657f412aa1953c4773076e593273f19d8e, 402c256d9a05e4ffb297d4ea1fc25d0230787bc0, 54af2b19a112070f10d191b98b055482748426a7, 894414a4a970822b9ecd59c0b6c480860707f636, 0fe32a259ef703c2de79135bfa6932a595fdc1c5) -- Add functionality for commit-message content analysis, such as NLP tools including stemming, tokenization, and lemmatization, as well as a function to search for keywords in commit messages and a function to measure the length of the messages (PR #281, 5aa4e4193f0c00095fedf961c6060a5c035ef9c6, 99f0638566c0062b987617bc3fe3ace1db7729ee, e469d3a0cf2881c378469b6ccfea9c204d13f19b, 7d8fd39f164c776921e3fb36daf79256e7be7426, ef689f71f248059cc69be4792ca14ce3b95dcac8, 6e642242a3063663bcc3c7f5cca0650dfebb6bb4) +- Add functionality for commit-message content analysis, such as NLP tools including stemming, tokenization, and lemmatization, as well as a function to search for keywords in commit messages and a function to measure the length of the messages (PR #281, 5aa4e4193f0c00095fedf961c6060a5c035ef9c6, 99f0638566c0062b987617bc3fe3ace1db7729ee, e469d3a0cf2881c378469b6ccfea9c204d13f19b, 7d8fd39f164c776921e3fb36daf79256e7be7426, ef689f71f248059cc69be4792ca14ce3b95dcac8, 6e642242a3063663bcc3c7f5cca0650dfebb6bb4, f54439486115cada08dac23864b2f7605edca9ea, dd9246b2f4506d3d58f1c1f37fc198aaaafebb0d) +- Deprecate support for R version 4.0 because of breaking dependencies (PR #281, 3dc91b155b3e0e2a55378592db448606381f902e) ### Changed/Improved From e706c7bef8418b04f27697cd9c0f6f2295be0abf Mon Sep 17 00:00:00 2001 From: Leo Sendelbach Date: Tue, 17 Jun 2025 16:00:37 +0200 Subject: [PATCH 47/92] Fix spelling errors pointed out in PR review Signed-off-by: Leo Sendelbach --- README.md | 2 +- util-data-misc.R | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b19ec246..01864702 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ In this section, we provide descriptions of the different algorithms we provide #### Commit-message content analysis -In this section, we give an overview of the functionalities we offer regarding the textual analysis of commit messages. These consist of basic NLP tasks, such as stemming (`get.stemmed.commit.messages`), tokenization (`get.tokenized.commit.messages`), and lemmatization (`get.lmmatized.commit.messages`), as well as preprocessing steps (`get.preprocessed.commit.messages`) such as lowercase transformation and removal of punctuation, stopwords, and extra whitespaces. Apart from these, there is the option of searching for a set of strings in commit messages for matching commits (`get.commit.messages.by.strings`) as well as getting token counts for commit messages (`get.commit.message.counts`). +In this section, we give an overview of the functionalities we offer regarding the textual analysis of commit messages. These consist of basic NLP tasks, such as stemming (`get.stemmed.commit.messages`), tokenization (`get.tokenized.commit.messages`), and lemmatization (`get.lemmatized.commit.messages`), as well as preprocessing steps (`get.preprocessed.commit.messages`) such as lowercase transformation and removal of punctuation, stopwords, and extra whitespaces. Apart from these, there is the option of searching for a set of strings in commit messages for matching commits (`get.commit.messages.by.strings`) as well as getting token counts for commit messages (`get.commit.message.counts`). ### How-to diff --git a/util-data-misc.R b/util-data-misc.R index 94a6c2b1..104bc1ca 100644 --- a/util-data-misc.R +++ b/util-data-misc.R @@ -32,7 +32,7 @@ requireNamespace("logging") # for logging requireNamespace("tm") # for NLP functionalities requireNamespace("SnowballC") # for text stemming used by NLP package "tm" requireNamespace("textstem") # for lemmatization -requireNamespace("parallel") # for for parallel computation +requireNamespace("parallel") # for parallel computation #' Helper function to mask all issues in the issue data frame. #' @@ -783,9 +783,9 @@ get.issue.is.pull.request = function(proj.data) { #' \code{"lowercase"} transforms all upper case characters into their lowercase counterparts #' \code{"stopwords"} removes all stopwords using a list of stopwords #' for the english language provided by the package \code{tm} -#' \code{"punctuaton"} removes all punctuation, as described in the ASCII \code{[:punct:]} class, +#' \code{"punctuation"} removes all punctuation, as described in the ASCII \code{[:punct:]} class, #' using the r-base \code{regex} functionality. This includes standard punctuation -#' characters such as ",", ".", ":", etc. but also dashes, parantheses, mathematical +#' characters such as ",", ".", ":", etc. but also dashes, parentheses, mathematical #' symbols and special characters used in programming, such as "$", "#", or "&". #' Intra-word dashes are kept. #' \code{"whitespaces"} removes superflous whitespace characters, such as "\t" or "\n", and replaces From 06a814c945f0b20af842d20247126083523cde55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 11:59:20 +0200 Subject: [PATCH 48/92] Refactor structure of arguments to 'merge.network.data' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 48 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/util-networks.R b/util-networks.R index d0167329..bd69b594 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1126,7 +1126,10 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## Merge network data and construct a vertex-only network for now: ## 1) construct vertex data (without edges) - vertex.data = merge.network.data(vertex.data = vertex.data, edge.data = NULL)[["vertices"]] + network.data = lapply(vertex.data, function(vertices) { + return(list(vertices = vertices, edges = NULL)) + }) + vertex.data = merge.network.data(network.data)[["vertices"]] ## 2) remove empty artifact, if names are available if ("name" %in% colnames(vertex.data)) { vertex.data = subset(vertex.data, !(name == UNTRACKED.FILE.EMPTY.ARTIFACT & type == TYPE.ARTIFACT)) @@ -1657,12 +1660,27 @@ construct.network.from.edge.list = function(vertices, edge.list, network.conf, d #' #' @param vertex.data the list of vertex data frames, may be \code{NULL} #' @param edge.data the list of edge data frames, may be \code{NULL} +#' @param network.data the vertex and edge data that should be merged. Each element +#' should be a list with two elements: vertices' and 'edges', that +#' contain the corresponding data for one network. #' #' @return list containing one edge data frame (name \code{edges}) and #' one vertex data frame (named \code{vertices}) -merge.network.data = function(vertex.data, edge.data) { +merge.network.data = function(network.data) { logging::logdebug("merge.network.data: starting.") + ## extract vertex and edge data + vertex.data = lapply(network.data, function(data) data[["vertices"]]) + edge.data = lapply(network.data, function(data) data[["edges"]]) + + if (length(network.data) == 1) { + logging::logdebug("Network data of only one network given, so a merge is not necessary") + return(list( + vertices = vertex.data[[1]], + edges = edge.data[[1]] + )) + } + ## combine vertices and select only unique vertices vertices = plyr::rbind.fill(vertex.data) vertices = unique.data.frame(vertices) @@ -1688,6 +1706,11 @@ merge.network.data = function(vertex.data, edge.data) { edges = create.empty.edge.list() } + ## catch case where no vertices (and no vertex attributes) are given + if (ncol(vertices) == 0) { + vertices = NULL # igraph::graph_from_data_frame fan handle this + } + logging::logdebug("merge.network.data: finished.") return(list( vertices = vertices, @@ -1711,23 +1734,16 @@ merge.networks = function(networks) { return(networks[[1]]) } - ## list with all vertex data frames - vertex.data = lapply(networks, function(network) { - return(igraph::as_data_frame(network, what = "vertices")) - }) - - ## list of all edge data frames - edge.data = lapply(networks, function(network) { - return(igraph::as_data_frame(network, what = "edges")) + ## construct network data + network.data = lapply(networks, function(network) { + return(list( + vertices = igraph::as_data_frame(network, what = "vertices"), + edges = igraph::as_data_frame(network, what = "edges") + )) }) ## merge all edge and vertex data frames - new.network.data = merge.network.data(vertex.data, edge.data) - - ## catch case where no vertices (and no vertex attributes) are given - if (ncol(new.network.data[["vertices"]]) == 0) { - new.network.data[["vertices"]] = NULL # igraph::graph_from_data_frame can handle this - } + new.network.data = merge.network.data(network.data) ## build whole network form edge and vertex data frame whole.network = igraph::graph_from_data_frame( From 4793eab02e8792b0640fad88a90018292b1b2ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 12:02:21 +0200 Subject: [PATCH 49/92] Introduce 'convert.edge.list.attributes.to.list' function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This function works analogous to 'convert.edge.attributes.to.list' but takes an edge list as input instead of a network. This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/util-networks.R b/util-networks.R index bd69b594..25ec9e64 100644 --- a/util-networks.R +++ b/util-networks.R @@ -2277,6 +2277,47 @@ convert.edge.attributes.to.list = function(network, remain.as.is = names(EDGE.AT return(network) } +#' Convert attributes in edge list to list type. +#' +#' @param edge.list the edge list either as a data frame or as a list of which +#' the attributes are to be converted +#' @param remain.as.is the attributes to remain as they are +#' [default: names(EDGE.ATTR.HANDLING)] +#' +#' @return the edge list with converted attributes as a data frame +#' +#' @seealso convert.edge.attributes.to.list +convert.edge.list.attributes.to.list = function(edge.list, remain.as.is = names(EDGE.ATTR.HANDLING)) { + + ## the 'from' and to 'to' columns must always remain as they are + remain.as.is = c(remain.as.is, "from", "to") + + ## if edge list is in list format, convert to data frame + if (is.list(edge.list)) { + edge.list = as.data.frame(edge.list, stringsAsFactors = FALSE) + } + + ## get edge attributes + edge.attrs = colnames(edge.list) + which.attrs = !(edge.attrs %in% remain.as.is) + + ## convert edge attributes to list type + for (attr in edge.attrs[which.attrs]) { + list.attr = as.list(edge.list[[attr]]) + + ## convert individual values to list + listed.values = sapply(list.attr, is.list) + if (!all(listed.values)) { + list.attr[!listed.values] = lapply(list.attr[!listed.values], as.list) + } + + ## replace attribute + edge.list[[attr]] = list.attr + } + + return(edge.list) + +} ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / ## Sample network ---------------------------------------------------------- From 8ba907fff0534c6fef39bd289ab163c90b053530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 12:03:23 +0200 Subject: [PATCH 50/92] Introduce private 'construct.network.data' function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This helper function creates a network data object from vertex data and edge data while correctly initializing empty input data. This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/util-networks.R b/util-networks.R index 25ec9e64..afba5f3b 100644 --- a/util-networks.R +++ b/util-networks.R @@ -176,6 +176,47 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", return(data.sources) }, + ## * * helper functions -------------------------------------------- + + #' Construct a network data object from (possibly empty) vertex and edge data. + construct.network.data = function(vertex.data, edge.data, allowed.edge.attributes) { + + all.edge.attributes = private$network.conf$get.value("edge.attributes") + + ## add missing vertex attributes + if (is.null(vertex.data) || nrow(vertex.data) == 0) { + vertex.data = data.frame(name = character(0)) + } + + ## add missing edge attributes if edgelist was empty + if (is.null(edge.data) || nrow(edge.data) == 0) { + + ## determine edge attributes to add + allowed.edge.attributes = lapply(allowed.edge.attributes, function(attr) attr[1]) + required.edge.attributes = all.edge.attributes[all.edge.attributes %in% names(allowed.edge.attributes)] + + ## construct empty edges with required edge attributes + edge.data = create.empty.data.frame( + c("from", "to", required.edge.attributes), + c("character", "character", allowed.edge.attributes[required.edge.attributes]) + ) + } + + ## convert edge attributes to list type + edge.data = convert.edge.list.attributes.to.list(edge.data) + + ## add weight attribute to edges + edge.data[["weight"]] = rep(1, nrow(edge.data)) + + ## construct network data + network.data = list( + vertices = vertex.data, + edges = edge.data + ) + + return(network.data) + }, + ## * * author networks --------------------------------------------- #' Get the co-change-based author relation as network. From 28d22902e32e93c0d4990576da2ef3de88fdffbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 12:06:41 +0200 Subject: [PATCH 51/92] Cache network data instead of networks for performance reasons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/util-networks.R b/util-networks.R index afba5f3b..88a67aeb 100644 --- a/util-networks.R +++ b/util-networks.R @@ -124,17 +124,18 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", proj.data.original = NULL, network.conf = NULL, - ## * * network caching --------------------------------------------- - - authors.network.mail = NULL, # igraph - authors.network.cochange = NULL, # igraph - authors.network.issue = NULL, #igraph - artifacts.network.cochange = NULL, # igraph - artifacts.network.callgraph = NULL, # igraph - artifacts.network.mail = NULL, # igraph - artifacts.network.issue = NULL, # igraph - commits.network.commit.interaction = NULL, #igraph - commits.network.cochange = NULL, #igraph + ## * * network data caching ---------------------------------------- + + author.network.mail.data = NULL, + author.network.cochange.data = NULL, + author.network.issue.data = NULL, + artifact.network.cochange.data = NULL, + artifact.network.callgraph.data = NULL, + artifact.network.mail.data = NULL, + artifact.network.issue.data = NULL, + artifact.network.commit.interaction.data = NULL, + commit.network.commit.interaction.data = NULL, + commit.network.cochange.data = NULL, ## * * relation-to-vertex-kind mapping ----------------------------- @@ -895,15 +896,15 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' Reset the current environment in order to rebuild it. #' Has to be called whenever the data or configuration get changed. reset.environment = function() { - private$authors.network.cochange = NULL - private$authors.network.issue = NULL - private$authors.network.mail = NULL - private$artifacts.network.callgraph = NULL - private$artifacts.network.cochange = NULL - private$artifacts.network.issue = NULL - private$artifacts.network.mail = NULL - private$commits.network.commit.interaction = NULL - private$commits.network.cochange = NULL + private$author.network.cochange.data = NULL + private$author.network.issue.data = NULL + private$author.network.mail.data = NULL + private$artifact.network.callgraph.data = NULL + private$artifact.network.cochange.data = NULL + private$artifact.network.issue.data = NULL + private$artifact.network.mail.data = NULL + private$commit.network.commit.interaction.data = NULL + private$commit.network.cochange.data = NULL private$proj.data = private$proj.data.original if (private$network.conf$get.value("unify.date.ranges")) { private$cut.data.to.same.timestamps() From 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 12:09:29 +0200 Subject: [PATCH 52/92] Adjust internal network creation functions to only return network data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 366 +++++++++++++++++++++++++----------------------- 1 file changed, 191 insertions(+), 175 deletions(-) diff --git a/util-networks.R b/util-networks.R index 88a67aeb..01b8198a 100644 --- a/util-networks.R +++ b/util-networks.R @@ -228,9 +228,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.author.network.cochange: starting.") ## do not compute anything more than once - if (!is.null(private$authors.network.cochange)) { + if (!is.null(private$author.network.cochange.data)) { logging::logdebug("get.author.network.cochange: finished. (already existing)") - return(private$authors.network.cochange) + return(private$author.network.cochange.data) } ## Get a list of all artifacts extracted from the commit data. Each artifact in this group is again a list @@ -264,29 +264,33 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", authors = authors["author.name"] ## 3) rename single column to "name" to correct mapping to vertex attribute "name" colnames(authors) = "name" - ## 4) set author list as vertices - author.net.data[["vertices"]] = authors - ## construct network from obtained data - author.net = construct.network.from.edge.list( - author.net.data[["vertices"]], - author.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("author.directed"), - available.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + ## construct network data + network.data = private$construct.network.data( + vertex.data = authors, + edge.data = author.net.data[["edges"]], + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") ) - ## store network - private$authors.network.cochange = author.net + ## store network data + private$author.network.cochange.data = network.data logging::logdebug("get.author.network.cochange: finished.") - return(author.net) + return(network.data) }, #' Build and get the author network with commit-interactions as the relation. #' #' @return the commit-interaction author network get.author.network.commit.interaction = function() { + logging::logdebug("get.author.network.commit.interaction: starting.") + + ## do not compute anything more than once + if (!is.null(private$author.network.commit.interaction.data)) { + logging::logdebug("get.author.network.commit.interaction: finished. (already existing)") + return(private$author.network.commit.interaction.data) + } + ## get the authors that appear in the commit-interaction data as the vertices of the network vertices = unique(c(private$proj.data$get.commit.interactions()[["base.author"]], private$proj.data$get.commit.interactions()[["interacting.author"]])) @@ -303,17 +307,19 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", if (nrow(edges) > 0) { edges[["artifact.type"]] = ARTIFACT.COMMIT.INTERACTION } - author.net.data = list(vertices = vertices, edges = edges) - ## construct the network - author.net = construct.network.from.edge.list( - author.net.data[["vertices"]], - author.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("author.directed"), - available.edge.attributes = private$proj.data$ - get.data.columns.for.data.source("commit.interactions") + + ## construct network data + network.data = private$construct.network.data( + vertex.data = vertices, + edge.data = edges, + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") ) - return(author.net) + + ## store network data + author.network.commit.interaction.data = network.data + logging::logdebug("get.author.network.commit.interaction: finished.") + + return(network.data) }, #' Get the thread-based author relation as network. @@ -325,9 +331,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.author.network.mail: starting.") ## do not compute anything more than once - if (!is.null(private$authors.network.mail)) { + if (!is.null(private$author.network.mail.data)) { logging::logdebug("get.author.network.mail: finished. (already existing)") - return(private$authors.network.mail) + return(private$author.network.mail.data) } ## construct edge list based on thread2author data @@ -338,29 +344,27 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", respect.temporal.order = private$network.conf$get.value("author.respect.temporal.order") ) - ## construct network from obtained data - author.net = construct.network.from.edge.list( - author.net.data[["vertices"]], - author.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("author.directed"), - available.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") + ## construct network data + network.data = private$construct.network.data( + vertex.data = author.net.data[["vertices"]], + edge.data = author.net.data[["edges"]], + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") ) - ## store network - private$authors.network.mail = author.net + ## store network data + private$author.network.mail.data = network.data logging::logdebug("get.author.network.mail: finished.") - return(author.net) + return(network.data) }, ##get the issue based author relation as network get.author.network.issue = function() { logging::logdebug("get.author.network.issue: starting.") - if (!is.null(private$authors.network.issue)) { + if (!is.null(private$author.network.issue.data)) { logging::logdebug("get.author.network.issue: finished. (already existing)") - return(private$authors.network.issue) + return(private$author.network.issue.data) } ## construct edge list based on issue2author data @@ -371,19 +375,18 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", respect.temporal.order = private$network.conf$get.value("author.respect.temporal.order") ) - ## construct network from obtained data - author.net = construct.network.from.edge.list( - author.net.data[["vertices"]], - author.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("author.directed"), - available.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") + ## construct network data + network.data = private$construct.network.data( + vertex.data = author.net.data[["vertices"]], + edge.data = author.net.data[["edges"]], + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") ) - private$authors.network.issue = author.net + ## store network data + private$author.network.issue.data = network.data logging::logdebug("get.author.network.issue: finished.") - return(author.net) + return(network.data) }, ## * * artifact networks ------------------------------------------- @@ -397,9 +400,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.artifact.network.cochange: starting.") ## do not compute anything more than once - if (!is.null(private$artifacts.network.cochange)) { + if (!is.null(private$artifact.network.cochange.data)) { logging::logdebug("get.artifact.network.cochange: finished. (already existing)") - return(private$artifacts.network.cochange) + return(private$artifact.network.cochange.data) } ## construct edge list based on commit--artifact data @@ -413,85 +416,102 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.type = "artifact" ) - ## construct network from obtained data - artifacts.net = construct.network.from.edge.list( - artifacts.net.data[["vertices"]], - artifacts.net.data[["edges"]], - network.conf = private$network.conf, - directed = FALSE, - available.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") - ) + ## extract vertices and edges + vertices = artifacts.net.data[["vertices"]] + edges = artifacts.net.data[["edges"]] ## remove the artifact vertices stemming from untracked files if existing - if ("name" %in% igraph::vertex_attr_names(artifacts.net) && - length(igraph::V(artifacts.net)[name == UNTRACKED.FILE.EMPTY.ARTIFACT]) > 0) { - - artifacts.net = igraph::delete_vertices(artifacts.net, UNTRACKED.FILE.EMPTY.ARTIFACT) + if ("name" %in% names(vertices) && any(vertices[["name"]] == UNTRACKED.FILE.EMPTY.ARTIFACT)) { + vertices = vertices[vertices[["name"]] != UNTRACKED.FILE.EMPTY.ARTIFACT, , drop = FALSE] + edges = edges[!(edges[["from"]] == UNTRACKED.FILE.EMPTY.ARTIFACT | + edges[["to"]] == UNTRACKED.FILE.EMPTY.ARTIFACT), ] } - ## store network - private$artifacts.network.cochange = artifacts.net + ## construct network data + network.data = private$construct.network.data( + vertex.data = vertices, + edge.data = edges, + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + ) + + ## store network data + private$artifact.network.cochange.data = network.data logging::logdebug("get.artifact.network.cochange: finished.") - return(artifacts.net) + return(network.data) }, #' Build and get the commit-interaction based artifact network. #' #' @return the commit-interaction based artifact network get.artifact.network.commit.interaction = function() { - ## initialize the vertices. They will be set correctly depending on the used config. - vertices = c() - ## get the commit-interaction data as the edge data of the network - edges = private$proj.data$get.commit.interactions() - - ## set 'to' and 'from' of the network according to the config - ## and order the dataframe accordingly - proj.conf.artifact = private$proj.data$get.project.conf.entry("artifact") - if (proj.conf.artifact == "file") { - ## change the vertices to the files from the commit-interaction data - vertices = unique(c(private$proj.data$get.commit.interactions()[["base.file"]], + + logging::logdebug("get.artifact.network.commit.interaction: starting.") + + ## do not compute anything more than once + if (!is.null(private$artifact.network.commit.interaction.data)) { + logging::logdebug("get.artifact.network.commit.interaction: finished. (already existing)") + return(private$artifact.network.commit.interaction.data) + } + + ## initialize the vertices. They will be set correctly depending on the used config. + vertices = c() + + ## get the commit-interaction data as the edge data of the network + edges = private$proj.data$get.commit.interactions() + + ## set 'to' and 'from' of the network according to the config + ## and order the dataframe accordingly + proj.conf.artifact = private$proj.data$get.project.conf.entry("artifact") + if (proj.conf.artifact == "file") { + + ## change the vertices to the files from the commit-interaction data + vertices = unique(c(private$proj.data$get.commit.interactions()[["base.file"]], private$proj.data$get.commit.interactions()[["file"]])) - vertices = data.frame(name = vertices) - - edges = edges[, c("file", "base.file", "func", "commit.hash", - "base.hash", "base.func", "base.author", "interacting.author")] - if (nrow(edges) > 0) { - edges[["artifact.type"]] = ARTIFACT.CODEFACE[[proj.conf.artifact]] - } - colnames(edges)[colnames(edges) == "commit.hash"] = "hash" - } else if (proj.conf.artifact == "function") { - ## change the vertices to the functions from the commit-interaction data - vertices = unique(c(private$proj.data$get.commit.interactions()[["base.func"]], - private$proj.data$get.commit.interactions()[["func"]])) - vertices = data.frame(name = vertices) - - edges = edges[, c("func", "base.func", "commit.hash", "file", "base.hash", - "base.file", "base.author", "interacting.author")] - if (nrow(edges) > 0) { - edges[["artifact.type"]] = ARTIFACT.CODEFACE[[proj.conf.artifact]] - } - colnames(edges)[colnames(edges) == "commit.hash"] = "hash" - } else { - ## If neither 'function' nor 'file' was configured, send a warning - ## and return an empty network - logging::logwarn("when creating a commit-interaction artifact network, - the artifact should be either 'file' or 'function'!") - return(create.empty.network(directed = private$network.conf$get.value("artifact.directed"))) - } - colnames(edges)[1] = "to" - colnames(edges)[2] = "from" - artifact.net.data = list(vertices = vertices, edges = edges) - ## construct the network - artifact.net = construct.network.from.edge.list( - artifact.net.data[["vertices"]], - artifact.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("artifact.directed"), - available.edge.attributes = private$proj.data$ - get.data.columns.for.data.source("commit.interactions") - ) - return(artifact.net) + vertices = data.frame(name = vertices) + + edges = edges[, c("file", "base.file", "func", "commit.hash", + "base.hash", "base.func", "base.author", "interacting.author")] + + } else if (proj.conf.artifact == "function") { + + ## change the vertices to the functions from the commit-interaction data + vertices = unique(c(private$proj.data$get.commit.interactions()[["base.func"]], + private$proj.data$get.commit.interactions()[["func"]])) + vertices = data.frame(name = vertices) + + edges = edges[, c("func", "base.func", "commit.hash", "file", "base.hash", + "base.file", "base.author", "interacting.author")] + + } else { + + ## If neither 'function' nor 'file' was configured, send a warning + ## and return an empty network + logging::logwarn("when creating a commit-interaction artifact network, + the artifact should be either 'file' or 'function'!") + return(create.empty.network(directed = private$network.conf$get.value("artifact.directed"))) + } + + if (nrow(edges) > 0) { + edges[["artifact.type"]] = ARTIFACT.CODEFACE[[proj.conf.artifact]] + } + colnames(edges)[colnames(edges) == "commit.hash"] = "hash" + + colnames(edges)[1] = "to" + colnames(edges)[2] = "from" + + ## construct network data + network.data = private$construct.network.data( + vertex.data = vertices, + edge.data = edges, + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") + ) + + ## store network data + private$artifact.network.commit.interaction.data = network.data + logging::logdebug("get.artifact.network.commit.interaction: finished.") + + return(network.data) }, #' Get the call-graph-based artifact network. @@ -504,9 +524,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.artifact.network.callgraph: starting.") ## do not compute anything more than once - if (!is.null(private$artifacts.network.callgraph)) { + if (!is.null(private$artifact.network.callgraph.data)) { logging::logdebug("get.artifact.network.callgraph: finished. (already existing)") - return(private$artifacts.network.callgraph) + return(private$artifact.network.callgraph.data) } ## check if revision for call-graphs is set @@ -570,11 +590,19 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", value = private$proj.data$get.project.conf.entry("artifact.codeface") ) - ## store network - private$artifacts.network.callgraph = artifacts.net + ## construct network data + network.data = private$construct.network.data( + vertex.data = igraph::as_data_frame(artifacts.net, "vertices"), + edge.data = igraph::as_data_frame(artifacts.net, "edges"), + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + ) + + ## store network data + private$artifact.network.callgraph.data = network.data logging::logdebug("get.artifact.network.callgraph: finished.") - return(artifacts.net) + return(network.data) + }, #' Get the mail-based artifact network. @@ -586,9 +614,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.artifact.network.mail: starting.") ## do not compute anything more than once - if (!is.null(private$artifacts.network.mail)) { + if (!is.null(private$artifact.network.mail.data)) { logging::logdebug("get.artifact.network.mail: finished. (already existing)") - return(private$artifacts.network.mail) + return(private$artifact.network.mail.data) } ## log warning as we do not have relations among threads right now @@ -597,17 +625,18 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", "Return an edge-less network now." )) - ## construct edgeless network with mandatory edge and vertex attributes - directed = private$network.conf$get.value("artifact.directed") - artifacts = private$proj.data$get.artifacts("mails") # thread IDs - artifacts.net = create.empty.network(directed = directed, add.attributes = TRUE) + - igraph::vertices(artifacts) + ## construct edgeless network data + network.data = private$construct.network.data( + vertex.data = data.frame(name = private$proj.data$get.artifacts("mails")), + edge.data = NULL, + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") + ) - ## store network - private$artifacts.network.mail = artifacts.net + ## store network data + private$artifact.network.mail.data = network.data logging::logdebug("get.artifact.network.mail: finished.") - return(artifacts.net) + return(network.data) }, #' Get the issue-based artifact network. @@ -619,9 +648,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.artifact.network.issue: starting.") ## do not compute anything more than once - if (!is.null(private$artifacts.network.issue)) { + if (!is.null(private$artifact.network.issue.data)) { logging::logdebug("get.artifact.network.issue: finished. (already existing)") - return(private$artifacts.network.issue) + return(private$artifact.network.issue.data) } if (private$proj.data$get.project.conf()$get.entry("issues.only.comments")) { @@ -690,7 +719,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logwarn("Inconsistent issue data. Unequally many 'add_link' and 'referenced_by' issue-events.") } - vertices = unique(artifacts.net.data.raw["issue.id"]) + vertices = unique(unlist(artifacts.net.data.raw["issue.id"])) edge.list = data.frame() # edges in artifact networks can not have the 'artifact' attribute but should instead have @@ -720,27 +749,18 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", } })) - artifacts.net.data = list( - vertices = data.frame( - name = vertices - ), - edges = edge.list - ) - - ## construct network from obtained data - artifacts.net = construct.network.from.edge.list( - artifacts.net.data[["vertices"]], - artifacts.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("artifact.directed"), - available.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") + ## construct network data + network.data = private$construct.network.data( + vertex.data = data.frame(name = vertices), + edge.data = edge.list, + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") ) - ## store network - private$artifacts.network.issue = artifacts.net + ## store network data + private$artifact.network.issue.data = network.data logging::logdebug("get.artifact.network.issue: finished.") - return(artifacts.net) + return(network.data) }, #' Build and get the commit network with commit-interactions as the relation. @@ -751,9 +771,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.commit.network.commit.interaction: starting.") ## do not compute anything more than once - if (!is.null(private$commits.network.commit.interaction)) { + if (!is.null(private$commit.network.commit.interaction.data)) { logging::logdebug("get.commit.network.commit.interaction: finished. (already existing)") - return(private$commits.network.commit.interaction) + return(private$commit.network.commit.interaction.data) } ## get the hashes that appear in the commit-interaction data as the vertices of the network @@ -771,21 +791,19 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", } colnames(edges)[1] = "to" colnames(edges)[2] = "from" - commit.net.data = list(vertices = vertices, edges = edges) - ## construct the network - commit.net = construct.network.from.edge.list( - commit.net.data[["vertices"]], - commit.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("commit.directed"), - available.edge.attributes = private$proj.data$ - get.data.columns.for.data.source("commit.interactions") + + ## construct network data + network.data = private$construct.network.data( + vertex.data = vertices, + edge.data = edges, + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") ) - private$commits.network.commit.interaction = commit.net + ## store network data + private$commit.network.commit.interaction.data = network.data logging::logdebug("get.commit.network.commit.interaction: finished.") - return(commit.net) + return(network.data) }, #' Get the cochange-based commit network, @@ -797,9 +815,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logdebug("get.commit.network.cochange: starting.") ## do not compute anything more than once - if (!is.null(private$commits.network.cochange)) { + if (!is.null(private$commit.network.cochange.data)) { logging::logdebug("get.commit.network.cochange: finished. (already existing)") - return(private$commits.network.cochange) + return(private$commit.network.cochange.data) } ## construct edge list based on commit--artifact data @@ -813,20 +831,18 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.type = "commit" ) - ## construct network from obtained data - commit.net = construct.network.from.edge.list( - commit.net.data[["vertices"]], - commit.net.data[["edges"]], - network.conf = private$network.conf, - directed = private$network.conf$get.value("commit.directed"), - available.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + ## construct network data + network.data = private$construct.network.data( + vertex.data = commit.net.data[["vertices"]], + edge.data = commit.net.data[["edges"]], + allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") ) - ## store network - private$commits.network.cochange = commit.net + ## store network data + private$commit.network.cochange.data = network.data logging::logdebug("get.commit.network.cochange: finished.") - return(commit.net) + return(network.data) }, ## * * bipartite relations ------------------------------------------ From b30c7f2b5b0a6d12e8024fafada5490170530ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 12:18:11 +0200 Subject: [PATCH 53/92] Ensure correct naming on 'from' and 'to' attribute of edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/util-networks.R b/util-networks.R index 01b8198a..198c6620 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1549,6 +1549,8 @@ construct.edges.temporal.order = function(set, network.conf, edge.attributes, ke } else { combinations = expand.grid(item.vertex, vertices.processed.set, stringsAsFactors = FALSE) } + colnames(combinations)[colnames(combinations) == "Var1"] = "from" + colnames(combinations)[colnames(combinations) == "Var2"] = "to" if (nrow(combinations) > 0 && nrow(item.edge.attrs) == 1) { combinations = cbind(combinations, item.edge.attrs, row.names = NULL) # add edge attributes From 1fa340d6347090a327b4c32ece705c1f700234e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 12:19:39 +0200 Subject: [PATCH 54/92] Simplify attribute conversion using new conversion function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/util-networks.R b/util-networks.R index 198c6620..2bae8c94 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1888,22 +1888,11 @@ add.edges.for.bipartite.relation = function(net, bipartite.relations, network.co extra.edge.attributes["type"] = TYPE.EDGES.INTER # add egde type extra.edge.attributes["relation"] = relation # add relation type - ## Convert edge attributes to list similarly to 'convert.edge.attributes.to.list'. - ## We cannot use 'convert.edge.attributes.to.list', as we operate on edge - ## data directly, instead of a network. - edge.attrs = names(extra.edge.attributes) - which.attrs = !(edge.attrs %in% names(EDGE.ATTR.HANDLING)) - for (attr in edge.attrs[which.attrs]) { - list.attr = as.list(extra.edge.attributes[[attr]]) - list.values = sapply(list.attr, is.list) - if (!all(list.values)) { - list.attr[!list.values] = lapply(list.attr[!list.values], as.list) - } - extra.edge.attributes[[attr]] = list.attr - } + ## convert the edge attributes to list format + edge.attributes = convert.edge.list.attributes.to.list(extra.edge.attributes) ## add the vertex sequences as edges to the network - net = igraph::add_edges(net, unlist(vertex.sequence.for.edges), attr = extra.edge.attributes) + net = igraph::add_edges(net, unlist(vertex.sequence.for.edges), attr = edge.attributes) ## replace NULLs in edge attributes with NAs for consistency net = Reduce(function(net, attr) { From 40cd55423be7b6521e2fc35f5aa200ff0594e77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 7 Apr 2025 12:24:29 +0200 Subject: [PATCH 55/92] Adjust public network creation functions to work with changed internals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'get.author.network', 'get.artifact.netork', and 'get.commit.network' now receive network data instead of networks from the internal network creation functions. Then these functions merges the received data instead of merging networks (like it used to be). This approach should improve performance as it removes the redundancy of decomposing networks in network data to merge them. This works towards fixing #119. Signed-off-by: Maximilian Löffler --- util-networks.R | 67 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/util-networks.R b/util-networks.R index 2bae8c94..c7b71a16 100644 --- a/util-networks.R +++ b/util-networks.R @@ -992,8 +992,8 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("author.relation") - networks = lapply(relations, function(relation) { - network = switch( + network.data = lapply(relations, function(relation) { + network.data = switch( relation, cochange = private$get.author.network.cochange(), commit.interaction = private$get.author.network.commit.interaction(), @@ -1004,12 +1004,20 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ) ## set edge attributes on all edges - igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = list(relation) + edge.count = nrow(network.data[["edges"]]) + network.data[["edges"]][["type"]] = rep(TYPE.EDGES.INTRA, edge.count) + network.data[["edges"]][["relation"]] = rep(list(list(relation)), edge.count) - return(network) + return(network.data) }) - net = merge.networks(networks) + merged.network.data = merge.network.data(network.data) + + ## construct graph from network data + net = igraph::graph_from_data_frame( + merged.network.data[["edges"]], + vertices = merged.network.data[["vertices"]], + directed = private$network.conf$get.value("author.directed") + ) ## add all missing authors to the network if wanted if (private$network.conf$get.value("author.all.authors")) { @@ -1046,7 +1054,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(net, "range") = private$proj.data$get.range() } - net = convert.edge.attributes.to.list(net) return(net) }, @@ -1058,8 +1065,8 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("artifact.relation") - networks = lapply(relations, function(relation) { - network = switch( + network.data = lapply(relations, function(relation) { + network.data = switch( relation, cochange = private$get.artifact.network.cochange(), callgraph = private$get.artifact.network.callgraph(), @@ -1070,16 +1077,24 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ) ## set edge attributes on all edges - igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = list(relation) + edge.count = nrow(network.data[["edges"]]) + network.data[["edges"]][["type"]] = rep(TYPE.EDGES.INTRA, edge.count) + network.data[["edges"]][["relation"]] = rep(list(list(relation)), edge.count) ## set vertex attribute 'kind' on all edges, corresponding to relation - vertex.kind = private$get.vertex.kind.for.relation(relation) - network = igraph::set_vertex_attr(network, "kind", value = vertex.kind) + vertex.count = nrow(network.data[["vertices"]]) + network.data[["vertices"]][["kind"]] = rep(private$get.vertex.kind.for.relation(relation), vertex.count) - return(network) + return(network.data) }) - net = merge.networks(networks) + merged.network.data = merge.network.data(network.data) + + ## construct graph from network data + net = igraph::graph_from_data_frame( + merged.network.data[["edges"]], + vertices = merged.network.data[["vertices"]], + directed = private$network.conf$get.value("artifact.directed") + ) ## set vertex and edge attributes for identifaction igraph::V(net)$type = TYPE.ARTIFACT @@ -1095,7 +1110,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(net, "range") = private$proj.data$get.range() } - net = convert.edge.attributes.to.list(net) return(net) }, @@ -1107,8 +1121,8 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("commit.relation") - networks = lapply(relations, function(relation) { - network = switch( + network.data = lapply(relations, function(relation) { + network.data = switch( relation, cochange = private$get.commit.network.cochange(), commit.interaction = private$get.commit.network.commit.interaction(), @@ -1116,12 +1130,20 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ) ## set edge attributes on all edges - igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = list(relation) + edge.count = nrow(network.data[["edges"]]) + network.data[["edges"]][["type"]] = rep(TYPE.EDGES.INTRA, edge.count) + network.data[["edges"]][["relation"]] = rep(list(list(relation)), edge.count) - return(network) + return(network.data) }) - net = merge.networks(networks) + merged.network.data = merge.network.data(network.data) + + ## construct graph from network data + net = igraph::graph_from_data_frame( + merged.network.data[["edges"]], + vertices = merged.network.data[["vertices"]], + directed = private$network.conf$get.value("commit.directed") + ) ## set vertex and edge attributes for identifaction igraph::V(net)$kind = TYPE.COMMIT @@ -1138,7 +1160,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(net, "range") = private$proj.data$get.range() } - net = convert.edge.attributes.to.list(net) return(net) }, From 8fcc74439c28b1592e964dd753bfc1cd57c062be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 8 Apr 2025 16:28:18 +0200 Subject: [PATCH 56/92] Complete and reorder the cached network data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-networks.R | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/util-networks.R b/util-networks.R index c7b71a16..ff83de92 100644 --- a/util-networks.R +++ b/util-networks.R @@ -126,16 +126,17 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## * * network data caching ---------------------------------------- - author.network.mail.data = NULL, author.network.cochange.data = NULL, + author.network.mail.data = NULL, author.network.issue.data = NULL, + author.network.commit.interaction.data = NULL, artifact.network.cochange.data = NULL, - artifact.network.callgraph.data = NULL, artifact.network.mail.data = NULL, artifact.network.issue.data = NULL, artifact.network.commit.interaction.data = NULL, - commit.network.commit.interaction.data = NULL, + artifact.network.callgraph.data = NULL, commit.network.cochange.data = NULL, + commit.network.commit.interaction.data = NULL, ## * * relation-to-vertex-kind mapping ----------------------------- @@ -913,14 +914,16 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' Has to be called whenever the data or configuration get changed. reset.environment = function() { private$author.network.cochange.data = NULL - private$author.network.issue.data = NULL private$author.network.mail.data = NULL - private$artifact.network.callgraph.data = NULL + private$author.network.issue.data = NULL + private$author.network.commit.interaction.data = NULL private$artifact.network.cochange.data = NULL - private$artifact.network.issue.data = NULL private$artifact.network.mail.data = NULL - private$commit.network.commit.interaction.data = NULL + private$artifact.network.issue.data = NULL + private$artifact.network.commit.interaction.data = NULL + private$artifact.network.callgraph.data = NULL private$commit.network.cochange.data = NULL + private$commit.network.commit.interaction.data = NULL private$proj.data = private$proj.data.original if (private$network.conf$get.value("unify.date.ranges")) { private$cut.data.to.same.timestamps() From ca348f1de8e3b4e5786a6d2726ca14e530446896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 8 Apr 2025 16:30:54 +0200 Subject: [PATCH 57/92] Improve documentation of 'construct.network.data' & 'merge.network.data' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-networks.R | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/util-networks.R b/util-networks.R index ff83de92..9e8bf476 100644 --- a/util-networks.R +++ b/util-networks.R @@ -181,7 +181,14 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## * * helper functions -------------------------------------------- #' Construct a network data object from (possibly empty) vertex and edge data. - construct.network.data = function(vertex.data, edge.data, allowed.edge.attributes) { + #' + #' @param vertex.data the vertex data frame + #' @param edge.data the edge data frame or NULL to represent an edgeless network [default: NULL] + #' @param allowed.edge.attributes a list of all attributes that are may be used in \code{edge.data} + #' depending on the data source of the represented network [default: NULL] + #' + #' @return the network data object + construct.network.data = function(vertex.data, edge.data = NULL, allowed.edge.attributes = NULL) { all.edge.attributes = private$network.conf$get.value("edge.attributes") @@ -629,7 +636,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct edgeless network data network.data = private$construct.network.data( vertex.data = data.frame(name = private$proj.data$get.artifacts("mails")), - edge.data = NULL, allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") ) @@ -1742,11 +1748,9 @@ construct.network.from.edge.list = function(vertices, edge.list, network.conf, d #' Note that identical vertices are merged, whereas identical edges are not. #' This will lead to duplicated edges if you merge a network with itself. #' -#' @param vertex.data the list of vertex data frames, may be \code{NULL} -#' @param edge.data the list of edge data frames, may be \code{NULL} -#' @param network.data the vertex and edge data that should be merged. Each element -#' should be a list with two elements: vertices' and 'edges', that -#' contain the corresponding data for one network. +#' @param network.data the network-describing data that should be merged. Each element +#' should be a list with two elements: \code{vertices} and \code{edges}, +#' that contain the corresponding data for one network. #' #' @return list containing one edge data frame (name \code{edges}) and #' one vertex data frame (named \code{vertices}) From 96ad0ca1d2a3e3174dbb27e69c3a3ac95852fbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 8 Apr 2025 16:32:59 +0200 Subject: [PATCH 58/92] Minor repairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-networks.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util-networks.R b/util-networks.R index 9e8bf476..6ad3d141 100644 --- a/util-networks.R +++ b/util-networks.R @@ -726,7 +726,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::logwarn("Inconsistent issue data. Unequally many 'add_link' and 'referenced_by' issue-events.") } - vertices = unique(unlist(artifacts.net.data.raw["issue.id"])) + vertices = unique(artifacts.net.data.raw[["issue.id"]]) edge.list = data.frame() # edges in artifact networks can not have the 'artifact' attribute but should instead have @@ -1796,7 +1796,7 @@ merge.network.data = function(network.data) { ## catch case where no vertices (and no vertex attributes) are given if (ncol(vertices) == 0) { - vertices = NULL # igraph::graph_from_data_frame fan handle this + vertices = NULL # igraph::graph_from_data_frame can handle this } logging::logdebug("merge.network.data: finished.") From 1d233af734f79e677d3388f7c3589ce186cc3a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 13 Apr 2025 11:11:42 +0200 Subject: [PATCH 59/92] Remove default parameters from 'construct.network.data' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A NULL default for 'edge.data' or 'allowed.edge.attributes' does not correctly represent the default use-case of 'construct.network.data'. Signed-off-by: Maximilian Löffler --- util-networks.R | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/util-networks.R b/util-networks.R index 6ad3d141..cf6a355b 100644 --- a/util-networks.R +++ b/util-networks.R @@ -183,12 +183,12 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' Construct a network data object from (possibly empty) vertex and edge data. #' #' @param vertex.data the vertex data frame - #' @param edge.data the edge data frame or NULL to represent an edgeless network [default: NULL] - #' @param allowed.edge.attributes a list of all attributes that are may be used in \code{edge.data} - #' depending on the data source of the represented network [default: NULL] + #' @param edge.data the edge data frame + #' @param allowed.edge.attributes a list of all attributes and their datatypes that should be present in + #' \code{edge.data} depending on the data source of the represented network #' #' @return the network data object - construct.network.data = function(vertex.data, edge.data = NULL, allowed.edge.attributes = NULL) { + construct.network.data = function(vertex.data, edge.data, allowed.edge.attributes) { all.edge.attributes = private$network.conf$get.value("edge.attributes") @@ -636,6 +636,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct edgeless network data network.data = private$construct.network.data( vertex.data = data.frame(name = private$proj.data$get.artifacts("mails")), + edge.data = NULL, allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") ) From 5dd5fc18940ce9ac9598902f175193167b471966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Wed, 16 Apr 2025 09:58:38 +0200 Subject: [PATCH 60/92] Maintain consistency in the naming of edge attribute related variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-networks.R | 79 +++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/util-networks.R b/util-networks.R index cf6a355b..abf832e7 100644 --- a/util-networks.R +++ b/util-networks.R @@ -184,13 +184,14 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' #' @param vertex.data the vertex data frame #' @param edge.data the edge data frame - #' @param allowed.edge.attributes a list of all attributes and their datatypes that should be present in - #' \code{edge.data} depending on the data source of the represented network + #' @param possible.edge.attributes a list of all possible attributes and their datatypes that could be present + #' in \code{edge.data} depending on the data source of the represented network. + #' This list is only used if \code{edge.data} is \code{NULL} or empty. #' #' @return the network data object - construct.network.data = function(vertex.data, edge.data, allowed.edge.attributes) { + construct.network.data = function(vertex.data, edge.data, possible.edge.attributes) { - all.edge.attributes = private$network.conf$get.value("edge.attributes") + configured.edge.attributes = private$network.conf$get.value("edge.attributes") ## add missing vertex attributes if (is.null(vertex.data) || nrow(vertex.data) == 0) { @@ -201,13 +202,14 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", if (is.null(edge.data) || nrow(edge.data) == 0) { ## determine edge attributes to add - allowed.edge.attributes = lapply(allowed.edge.attributes, function(attr) attr[1]) - required.edge.attributes = all.edge.attributes[all.edge.attributes %in% names(allowed.edge.attributes)] + possible.edge.attributes = lapply(possible.edge.attributes, function(attr) attr[1]) + required.edge.attributes = configured.edge.attributes[configured.edge.attributes %in% + names(possible.edge.attributes)] ## construct empty edges with required edge attributes edge.data = create.empty.data.frame( c("from", "to", required.edge.attributes), - c("character", "character", allowed.edge.attributes[required.edge.attributes]) + c("character", "character", possible.edge.attributes[required.edge.attributes]) ) } @@ -277,7 +279,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = authors, edge.data = author.net.data[["edges"]], - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") ) ## store network data @@ -320,7 +322,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = vertices, edge.data = edges, - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") ) ## store network data @@ -356,7 +358,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = author.net.data[["vertices"]], edge.data = author.net.data[["edges"]], - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") ) ## store network data @@ -387,7 +389,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = author.net.data[["vertices"]], edge.data = author.net.data[["edges"]], - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") ) ## store network data @@ -439,7 +441,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = vertices, edge.data = edges, - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") ) ## store network data @@ -512,7 +514,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = vertices, edge.data = edges, - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") ) ## store network data @@ -602,7 +604,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = igraph::as_data_frame(artifacts.net, "vertices"), edge.data = igraph::as_data_frame(artifacts.net, "edges"), - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") ) ## store network data @@ -637,7 +639,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = data.frame(name = private$proj.data$get.artifacts("mails")), edge.data = NULL, - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("mails") ) ## store network data @@ -761,7 +763,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = data.frame(name = vertices), edge.data = edge.list, - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("issues") ) ## store network data @@ -804,7 +806,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = vertices, edge.data = edges, - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("commit.interactions") ) ## store network data @@ -843,7 +845,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", network.data = private$construct.network.data( vertex.data = commit.net.data[["vertices"]], edge.data = commit.net.data[["edges"]], - allowed.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") + possible.edge.attributes = private$proj.data$get.data.columns.for.data.source("commits") ) ## store network data @@ -1224,7 +1226,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", vertex.data = subset(vertex.data, !(name == UNTRACKED.FILE.EMPTY.ARTIFACT & type == TYPE.ARTIFACT)) } ## 3) obtain all possible data columns, i.e., edge attributes - available.edge.attributes = lapply( + possible.edge.attributes = lapply( private$network.conf$get.variable("artifact.relation"), function(relation) { data.source = RELATION.TO.DATASOURCE[[relation]] @@ -1232,9 +1234,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", return(data.cols) } ) - available.edge.attributes = unlist(available.edge.attributes, recursive = FALSE) - available.edge.attributes = available.edge.attributes[ - !duplicated(names(available.edge.attributes)) # remove duplicates based on names + possible.edge.attributes = unlist(possible.edge.attributes, recursive = FALSE) + possible.edge.attributes = possible.edge.attributes[ + !duplicated(names(possible.edge.attributes)) # remove duplicates based on names ] ## 4) construct network without edges vertex.network = construct.network.from.edge.list( @@ -1242,7 +1244,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", edge.list = create.empty.edge.list(), network.conf = private$network.conf, directed = directed, - available.edge.attributes = available.edge.attributes + possible.edge.attributes = possible.edge.attributes ) ## explicitly add vertex attributes if vertex data was empty @@ -1257,7 +1259,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", vertex.network, bipartite.relations = bipartite.relation.data, private$network.conf, - available.edge.attributes = available.edge.attributes + possible.edge.attributes = possible.edge.attributes ) ## remove vertices that are not committers if wanted @@ -1698,7 +1700,7 @@ construct.edges.no.temporal.order = function(set, network.conf, edge.attributes, #' #' @return the built network construct.network.from.edge.list = function(vertices, edge.list, network.conf, directed = FALSE, - available.edge.attributes = list()) { + possible.edge.attributes = list()) { logging::logdebug("construct.network.from.edge.list: starting.") logging::loginfo("Construct network from edges.") @@ -1728,10 +1730,11 @@ construct.network.from.edge.list = function(vertices, edge.list, network.conf, d ## add missing edge attributes if edge.list was empty (igraph::graph_from_data_frame does add them then) if (nrow(edge.list) == 0) { ## edge attributes - allowed.attributes = network.conf$get.value("edge.attributes") - needed.edge.attributes = allowed.attributes[allowed.attributes %in% names(available.edge.attributes)] - needed.edge.attributes.types = available.edge.attributes[needed.edge.attributes] - net = add.attributes.to.network(net, "edge", needed.edge.attributes.types) + configured.edge.attributes = network.conf$get.value("edge.attributes") + required.edge.attributes = configured.edge.attributes[configured.edge.attributes %in% + names(possible.edge.attributes)] + required.edge.attributes.types = possible.edge.attributes[required.edge.attributes] + net = add.attributes.to.network(net, "edge", required.edge.attributes.types) } ## initialize edge weights @@ -1852,12 +1855,12 @@ merge.networks = function(networks) { #' @param bipartite.relations the list of the vertex relations to add to the given network for #' all configured relations #' @param network.conf the network configuration -#' @param available.edge.attributes a named vector/list of attribute classes, with their corresponding names -#' as names on the list [default: list()] +#' @param possible.edge.attributes a named vector/list of attribute classes, with their corresponding names +#' as names on the list [default: list()] #' #' @return the adjusted network add.edges.for.bipartite.relation = function(net, bipartite.relations, network.conf, - available.edge.attributes = list()) { + possible.edge.attributes = list()) { ## iterate about all bipartite.relations depending on the relation type for (relation in names(bipartite.relations)) { @@ -1879,10 +1882,10 @@ add.edges.for.bipartite.relation = function(net, bipartite.relations, network.co }, names(net1.to.net2), net1.to.net2, SIMPLIFY = FALSE) ## initialize edge attributes - allowed.edge.attributes = network.conf$get.value("edge.attributes") - available.edge.attributes = available.edge.attributes[names(available.edge.attributes) - %in% allowed.edge.attributes] - net = add.attributes.to.network(net, "edge", allowed.edge.attributes) + configured.edge.attributes = network.conf$get.value("edge.attributes") + possible.edge.attributes = possible.edge.attributes[names(possible.edge.attributes) + %in% configured.edge.attributes] + net = add.attributes.to.network(net, "edge", configured.edge.attributes) ## get extra edge attributes extra.edge.attributes.df = parallel::mcmapply(vertex.sequence = vertex.sequence.for.edges, a.df = net1.to.net2, @@ -1908,8 +1911,8 @@ add.edges.for.bipartite.relation = function(net, bipartite.relations, network.co } ## select the allowed attributes from the edge data.frame's columns - cols.which = allowed.edge.attributes %in% colnames(constructed.edges) - return(constructed.edges[ , allowed.edge.attributes[cols.which], drop = FALSE]) + cols.which = configured.edge.attributes %in% colnames(constructed.edges) + return(constructed.edges[ , configured.edge.attributes[cols.which], drop = FALSE]) }) extra.edge.attributes.df = plyr::rbind.fill(extra.edge.attributes.df) extra.edge.attributes = as.list(extra.edge.attributes.df) From a9f19f7f04a7c22193e8381f13b3de9d85d8ac5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Wed, 16 Apr 2025 22:26:29 +0200 Subject: [PATCH 61/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index f3eb2459..40ba060f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,6 +13,8 @@ ### Changed/Improved - For consistency reasons: Ensure that the values of edge attributes are always lists even when they represent singular values (PR #278, 6fae1843740ed8e48c89c2ee4e61f995b5d0b8f5, 416c817998540fc0b82d9959574838b571b4d6fb) +- Reduce the amount of redundantly built networks by caching network data internally. This should improve the performance of building multi-networks, especially, when parts of the multi-networks have been built before (#119, PR #282, 64ac42aa743e7f3a724a66bcd551e5b477e30293, 1eda73265a3553e7a785a180118b1c872aeec091, beed2cc9f75619065afce1992a62ecd8ae942ce3, e2dc9954a526eb7ccb1de87571400cbbb8abb76e, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e, 231bc479f7bc04ce4048c6633bae1f1cb15307ca, a64835618f7868c93f6a0b42e196d56cedc799a7, 7537d800542d1bbdd6d3ebcc97472d58a73cbf77, 82fc4cf592c86b1a5ae161ab662433050ef5ce66, f8093033a697472679be045cabda6c1f0197b168, 9c739c0e837184f49d2fe4afb89b1eabf145d912, d6cccd7386cf0c44bfafbd684b454ab7bd1f21c2) +- Internally cache commit-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e) ### Fixed From 1b156c17f261d8b70d8d48c6cb94d3ee591559f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 24 Apr 2025 10:34:26 +0200 Subject: [PATCH 62/92] Adjust list of allowed edge attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'author.name' and 'author.mail' are redundant and can be removed. While we currently do not build networks that have 'event.info.1' or 'event.info.2' edge attributes these attributes are present on the source data and should be allowed edge attributes. Signed-off-by: Maximilian Löffler --- util-conf.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util-conf.R b/util-conf.R index eee1f16d..281d2f3a 100644 --- a/util-conf.R +++ b/util-conf.R @@ -27,6 +27,7 @@ ## Copyright 2021 by Mirabdulla Yusifli ## Copyright 2022 by Jonathan Baumann ## Copyright 2024 by Leo Sendelbach +## Copyright 2025 by Maximilian Löffler ## All Rights Reserved. @@ -883,7 +884,7 @@ NetworkConf = R6::R6Class("NetworkConf", inherit = Conf, "pasta", # issue information "issue.id", "issue.state", "creation.date", "closing.date", "is.pull.request", - "author.name", "author.mail", "event.date", "event.name" + "event.date", "event.name", "event.info.1", "event.info.2" ), allowed.number = Inf ), From b8a72efa521a146c775b0a3f9e2f42846371bba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 24 Apr 2025 10:44:16 +0200 Subject: [PATCH 63/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 40ba060f..58e8be17 100644 --- a/NEWS.md +++ b/NEWS.md @@ -15,6 +15,7 @@ - For consistency reasons: Ensure that the values of edge attributes are always lists even when they represent singular values (PR #278, 6fae1843740ed8e48c89c2ee4e61f995b5d0b8f5, 416c817998540fc0b82d9959574838b571b4d6fb) - Reduce the amount of redundantly built networks by caching network data internally. This should improve the performance of building multi-networks, especially, when parts of the multi-networks have been built before (#119, PR #282, 64ac42aa743e7f3a724a66bcd551e5b477e30293, 1eda73265a3553e7a785a180118b1c872aeec091, beed2cc9f75619065afce1992a62ecd8ae942ce3, e2dc9954a526eb7ccb1de87571400cbbb8abb76e, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e, 231bc479f7bc04ce4048c6633bae1f1cb15307ca, a64835618f7868c93f6a0b42e196d56cedc799a7, 7537d800542d1bbdd6d3ebcc97472d58a73cbf77, 82fc4cf592c86b1a5ae161ab662433050ef5ce66, f8093033a697472679be045cabda6c1f0197b168, 9c739c0e837184f49d2fe4afb89b1eabf145d912, d6cccd7386cf0c44bfafbd684b454ab7bd1f21c2) - Internally cache commit-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e) +- Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, ad3f8b3e82040a613e91a1744436a2fbdd74fe8d) ### Fixed From 65ead39b7b971e5a0acbaee4e787efcf194aafc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 15 May 2025 18:19:28 +0200 Subject: [PATCH 64/92] Enforce correct directedness when building networks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The directedness of the edge construction algorithm should always align with the directedness of the constructed network. Further, when one sub-network of a multi-network requires (un)directed edge construction, all sub-networks of that multi-network must inherit this directedness. Signed-off-by: Maximilian Löffler --- util-networks.R | 142 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 34 deletions(-) diff --git a/util-networks.R b/util-networks.R index abf832e7..abb3ab98 100644 --- a/util-networks.R +++ b/util-networks.R @@ -94,6 +94,14 @@ RELATION.TO.DATASOURCE = list( "issue" = "issues" ) +## A value of \code{TRUE} indicates that the corresponding network will be built using the directed edge +## construction algorithm analogous \code{FALSE} means the undirected edge construction algorithm is used +ENFORCED.DIRECTEDNESS = list( + "author" = list(), + "artifact" = list("cochange" = FALSE), + "commit" = list() +) + ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / ## NetworkBuilder ---------------------------------------------------------- @@ -234,7 +242,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' If it does not already exist build it first. #' #' @return the author network with cochange relation - get.author.network.cochange = function() { + get.author.network.cochange = function(directed) { logging::logdebug("get.author.network.cochange: starting.") ## do not compute anything more than once @@ -260,7 +268,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", author.net.data = construct.edge.list.from.key.value.list( author.groups, network.conf = private$network.conf, - directed = private$network.conf$get.value("author.directed"), + directed = directed, respect.temporal.order = private$network.conf$get.value("author.respect.temporal.order") ) @@ -292,7 +300,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' Build and get the author network with commit-interactions as the relation. #' #' @return the commit-interaction author network - get.author.network.commit.interaction = function() { + get.author.network.commit.interaction = function(directed) { logging::logdebug("get.author.network.commit.interaction: starting.") ## do not compute anything more than once @@ -336,7 +344,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' If it does not already exist build it first. #' #' @return the author network with mail relation - get.author.network.mail = function() { + get.author.network.mail = function(directed) { logging::logdebug("get.author.network.mail: starting.") @@ -350,7 +358,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", author.net.data = construct.edge.list.from.key.value.list( private$proj.data$group.authors.by.data.column("mails", "thread"), network.conf = private$network.conf, - directed = private$network.conf$get.value("author.directed"), + directed = directed, respect.temporal.order = private$network.conf$get.value("author.respect.temporal.order") ) @@ -369,7 +377,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", }, ##get the issue based author relation as network - get.author.network.issue = function() { + get.author.network.issue = function(directed) { logging::logdebug("get.author.network.issue: starting.") if (!is.null(private$author.network.issue.data)) { @@ -381,7 +389,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", author.net.data = construct.edge.list.from.key.value.list( private$proj.data$group.authors.by.data.column("issues", "issue.id"), network.conf = private$network.conf, - directed = private$network.conf$get.value("author.directed"), + directed = directed, respect.temporal.order = private$network.conf$get.value("author.respect.temporal.order") ) @@ -405,7 +413,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' If it does not already exist build it first. #' #' @return the artifact network with cochange realtion - get.artifact.network.cochange = function() { + get.artifact.network.cochange = function(directed) { logging::logdebug("get.artifact.network.cochange: starting.") @@ -421,7 +429,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", artifacts.net.data = construct.edge.list.from.key.value.list( artifacts.net.data.raw, network.conf = private$network.conf, - directed = FALSE, + directed = directed, respect.temporal.order = TRUE, network.type = "artifact" ) @@ -454,7 +462,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' Build and get the commit-interaction based artifact network. #' #' @return the commit-interaction based artifact network - get.artifact.network.commit.interaction = function() { + get.artifact.network.commit.interaction = function(directed) { logging::logdebug("get.artifact.network.commit.interaction: starting.") @@ -499,7 +507,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## and return an empty network logging::logwarn("when creating a commit-interaction artifact network, the artifact should be either 'file' or 'function'!") - return(create.empty.network(directed = private$network.conf$get.value("artifact.directed"))) + return(create.empty.network(directed = directed)) } if (nrow(edges) > 0) { @@ -529,7 +537,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' IMPORTANT: This only works for range-level analyses! #' #' @return the artifact network with callgraph relation - get.artifact.network.callgraph = function() { + get.artifact.network.callgraph = function(directed) { logging::logdebug("get.artifact.network.callgraph: starting.") @@ -619,7 +627,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' If it does not already exist build it first. #' #' @return the artifact network with mail relation - get.artifact.network.mail = function() { + get.artifact.network.mail = function(directed) { logging::logdebug("get.artifact.network.mail: starting.") @@ -653,7 +661,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' If it does not already exist build it first. #' #' @return the artifact network with issue relation - get.artifact.network.issue = function() { + get.artifact.network.issue = function(directed) { logging::logdebug("get.artifact.network.issue: starting.") @@ -683,7 +691,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## to the referencing issue, in addition to the correct events, linking the referencing issue to ## the referenced issue. We can only deduplicate them, if we build an undirected network, as otherwise, ## we would need to guess the correct direction. - if (!private$network.conf$get.entry("artifact.directed")) { + if (!directed) { ## obtain 'add_link' events from jira jira.add.links = add.links[add.links$issue.source == "jira", ] @@ -776,7 +784,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' Build and get the commit network with commit-interactions as the relation. #' #' @return the commit-interaction commit network - get.commit.network.commit.interaction = function() { + get.commit.network.commit.interaction = function(directed) { logging::logdebug("get.commit.network.commit.interaction: starting.") @@ -820,7 +828,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' If it does not already exist build it first. #' #' @return the commit network with cochange realtion - get.commit.network.cochange = function() { + get.commit.network.cochange = function(directed) { logging::logdebug("get.commit.network.cochange: starting.") @@ -836,7 +844,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", commit.net.data = construct.edge.list.from.key.value.list( commit.net.data.raw, network.conf = private$network.conf, - directed = private$network.conf$get.value("commit.directed"), + directed = directed, respect.temporal.order = TRUE, network.type = "commit" ) @@ -1002,15 +1010,37 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", get.author.network = function() { logging::loginfo("Constructing author network.") - ## construct network relations = private$network.conf$get.value("author.relation") + + ## Determine directedness + enforced.directedness = ENFORCE.DIRECTEDNESS[["author"]] + enforced.directedness = enforced.directedness[names(enforced.directedness) %in% relations] + + if (length(enforced.directedness) > 0) { + + ## If at least one network enforces undirectedness, all networks need to be undirected + directed = all(enforced.directedness) + + if (directed != private$network.conf$get.value("author.directed")) { + notification.string = paste("The enforced directedness for the construction of the author", + "network differs from the configured directedness. Enforced", + "directedness: %s, configured directedness: %s") + logging::logdebug(notification.string, directed, private$network.conf$get.value("author.directed")) + } + + } else { + ## If no directedness is enforced, use the configured value + directed = private$network.conf$get.value("author.directed") + } + + ## construct network network.data = lapply(relations, function(relation) { network.data = switch( relation, - cochange = private$get.author.network.cochange(), - commit.interaction = private$get.author.network.commit.interaction(), - mail = private$get.author.network.mail(), - issue = private$get.author.network.issue(), + cochange = private$get.author.network.cochange(directed), + commit.interaction = private$get.author.network.commit.interaction(directed), + mail = private$get.author.network.mail(directed), + issue = private$get.author.network.issue(directed), stop(sprintf("The author relation '%s' does not exist.", rel)) ## TODO construct edge lists here and merge those (inline the private methods) ) @@ -1028,7 +1058,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", net = igraph::graph_from_data_frame( merged.network.data[["edges"]], vertices = merged.network.data[["vertices"]], - directed = private$network.conf$get.value("author.directed") + directed = directed ) ## add all missing authors to the network if wanted @@ -1077,14 +1107,36 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("artifact.relation") + + ## Determine directedness + enforced.directedness = ENFORCE.DIRECTEDNESS[["artifact"]] + enforced.directedness = enforced.directedness[names(enforced.directedness) %in% relations] + + if (length(enforced.directedness) > 0) { + + ## If at least one network enforces undirectedness, all networks need to be undirected + directed = all(enforced.directedness) + + if (directed != private$network.conf$get.value("artifact.directed")) { + notification.string = paste("The enforced directedness for the construction of the artifact", + "network differs from the configured directedness. Enforced", + "directedness: %s, configured directedness: %s") + logging::logdebug(notification.string, directed, private$network.conf$get.value("artifact.directed")) + } + + } else { + ## If no directedness is enforced, use the configured value + directed = private$network.conf$get.value("artifact.directed") + } + network.data = lapply(relations, function(relation) { network.data = switch( relation, - cochange = private$get.artifact.network.cochange(), - callgraph = private$get.artifact.network.callgraph(), - mail = private$get.artifact.network.mail(), - issue = private$get.artifact.network.issue(), - commit.interaction = private$get.artifact.network.commit.interaction(), + cochange = private$get.artifact.network.cochange(directed), + callgraph = private$get.artifact.network.callgraph(directed), + mail = private$get.artifact.network.mail(directed), + issue = private$get.artifact.network.issue(directed), + commit.interaction = private$get.artifact.network.commit.interaction(directed), stop(sprintf("The artifact relation '%s' does not exist.", relation)) ) @@ -1105,7 +1157,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", net = igraph::graph_from_data_frame( merged.network.data[["edges"]], vertices = merged.network.data[["vertices"]], - directed = private$network.conf$get.value("artifact.directed") + directed = directed ) ## set vertex and edge attributes for identifaction @@ -1133,11 +1185,33 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("commit.relation") + + ## Determine directedness + enforced.directedness = ENFORCE.DIRECTEDNESS[["commit"]] + enforced.directedness = enforced.directedness[names(enforced.directedness) %in% relations] + + if (length(enforced.directedness) > 0) { + + ## If at least one network enforces undirectedness, all networks need to be undirected + directed = all(enforced.directedness) + + if (directed != private$network.conf$get.value("commit.directed")) { + notification.string = paste("The enforced directedness for the construction of the commit", + "network differs from the configured directedness. Enforced", + "directedness: %s, configured directedness: %s") + logging::logdebug(notification.string, directed, private$network.conf$get.value("commit.directed")) + } + + } else { + ## If no directedness is enforced, use the configured value + directed = private$network.conf$get.value("commit.directed") + } + network.data = lapply(relations, function(relation) { network.data = switch( relation, - cochange = private$get.commit.network.cochange(), - commit.interaction = private$get.commit.network.commit.interaction(), + cochange = private$get.commit.network.cochange(directed), + commit.interaction = private$get.commit.network.commit.interaction(directed), stop(sprintf("The commit relation '%s' does not exist.", relation)) ) @@ -1154,7 +1228,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", net = igraph::graph_from_data_frame( merged.network.data[["edges"]], vertices = merged.network.data[["vertices"]], - directed = private$network.conf$get.value("commit.directed") + directed = directed ) ## set vertex and edge attributes for identifaction @@ -1181,7 +1255,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", get.bipartite.network = function() { ## get data by the chosen relation bipartite.relation.data = private$get.bipartite.relations() - directed = private$network.conf$get.value("author.directed") + directed = private$determine.directedness("author") vertex.data = lapply(bipartite.relation.data, function(net.to.net) { From a776caf72256200e1bfa5578106a9b53547b00e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 15 May 2025 22:03:57 +0200 Subject: [PATCH 65/92] Collect logic regarding directedness in 'determine.directedness' method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-networks.R | 118 ++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 63 deletions(-) diff --git a/util-networks.R b/util-networks.R index abb3ab98..6bd821cd 100644 --- a/util-networks.R +++ b/util-networks.R @@ -236,6 +236,57 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", return(network.data) }, + #' Determine the directedness a to-be-built network should have + #' based on the configured and enforced directedness + #' + #' @param network.type the type of network to build default: [c("author", "artifact", "commit")] + #' + #' @return the inferred directedness of the network + determine.directedness = function(network.type = c("author", "artifact", "commit")) { + + network.type = match.arg.or.default(network.type, default = "author", several.ok = TRUE) + + ## collect configured and enforced directedness + configured.directedness = list() + enforced.directedness = list() + + for (type in network.type) { + + ## get enforced directedness + relations = private$network.conf$get.value(paste0(type, ".relation")) + enforced = ENFORCED.DIRECTEDNESS[[type]] + enforced = enforced[names(enforced) %in% relations] + enforced.directedness[[type]] = enforced + + ## get configured directedness + configured = private$network.conf$get.value(paste0(type, ".directed")) + configured.directedness[[type]] = configured + } + + ## if at least one network enforces undirectedness all networks need to be undirected, + ## i.e., \code{directed} can only be \code{TRUE} if all enforced directedness are \code{TRUE} + if (any(sapply(enforced.directedness, length) > 0)) { + directed = all(unlist(enforced.directedness)) + } + + ## if no directedness is enforced, use the configured values + ## if at least one network is configured to be undirected all networks need to be undirected + else { + directed = all(unlist(configured.directedness)) + } + + ## print a warning if some configured directedness differ from the enforced directedness + overwritten.configurations = names(configured.directedness)[configured.directedness != directed] + if (length(overwritten.configurations) > 0) { + notification.string = paste("The enforced directedness for the construction of the %s", + "network(s) differs from the configured directedness. Enforced", + "directedness: %s, configured directedness: %s") + logging::logwarn(notification.string, overwritten.configurations, directed, !directed) + } + + return(directed) + }, + ## * * author networks --------------------------------------------- #' Get the co-change-based author relation as network. @@ -1011,27 +1062,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", logging::loginfo("Constructing author network.") relations = private$network.conf$get.value("author.relation") - - ## Determine directedness - enforced.directedness = ENFORCE.DIRECTEDNESS[["author"]] - enforced.directedness = enforced.directedness[names(enforced.directedness) %in% relations] - - if (length(enforced.directedness) > 0) { - - ## If at least one network enforces undirectedness, all networks need to be undirected - directed = all(enforced.directedness) - - if (directed != private$network.conf$get.value("author.directed")) { - notification.string = paste("The enforced directedness for the construction of the author", - "network differs from the configured directedness. Enforced", - "directedness: %s, configured directedness: %s") - logging::logdebug(notification.string, directed, private$network.conf$get.value("author.directed")) - } - - } else { - ## If no directedness is enforced, use the configured value - directed = private$network.conf$get.value("author.directed") - } + directed = private$determine.directedness("author") ## construct network network.data = lapply(relations, function(relation) { @@ -1107,27 +1138,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("artifact.relation") - - ## Determine directedness - enforced.directedness = ENFORCE.DIRECTEDNESS[["artifact"]] - enforced.directedness = enforced.directedness[names(enforced.directedness) %in% relations] - - if (length(enforced.directedness) > 0) { - - ## If at least one network enforces undirectedness, all networks need to be undirected - directed = all(enforced.directedness) - - if (directed != private$network.conf$get.value("artifact.directed")) { - notification.string = paste("The enforced directedness for the construction of the artifact", - "network differs from the configured directedness. Enforced", - "directedness: %s, configured directedness: %s") - logging::logdebug(notification.string, directed, private$network.conf$get.value("artifact.directed")) - } - - } else { - ## If no directedness is enforced, use the configured value - directed = private$network.conf$get.value("artifact.directed") - } + directed = private$determine.directedness("artifact") network.data = lapply(relations, function(relation) { network.data = switch( @@ -1185,27 +1196,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("commit.relation") - - ## Determine directedness - enforced.directedness = ENFORCE.DIRECTEDNESS[["commit"]] - enforced.directedness = enforced.directedness[names(enforced.directedness) %in% relations] - - if (length(enforced.directedness) > 0) { - - ## If at least one network enforces undirectedness, all networks need to be undirected - directed = all(enforced.directedness) - - if (directed != private$network.conf$get.value("commit.directed")) { - notification.string = paste("The enforced directedness for the construction of the commit", - "network differs from the configured directedness. Enforced", - "directedness: %s, configured directedness: %s") - logging::logdebug(notification.string, directed, private$network.conf$get.value("commit.directed")) - } - - } else { - ## If no directedness is enforced, use the configured value - directed = private$network.conf$get.value("commit.directed") - } + directed = private$determine.directedness("commit") network.data = lapply(relations, function(relation) { network.data = switch( @@ -2474,6 +2465,7 @@ convert.edge.list.attributes.to.list = function(edge.list, remain.as.is = names( } + ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / ## Sample network ---------------------------------------------------------- From 257a1c8a6a9b1c3e2a72960cc4051a87950753ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 15 May 2025 22:05:08 +0200 Subject: [PATCH 66/92] Use correct(ed) directedness in 'get.multi.network' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-networks.R | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/util-networks.R b/util-networks.R index 6bd821cd..4b3feb3b 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1385,8 +1385,20 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", get.multi.network = function() { logging::loginfo("Constructing multi network.") - ## construct the network parts we need for the multi network + ## stash configured directedness + configured.author.directedness = private$network.conf$get.value("author.directed") + configured.artifact.directedness = private$network.conf$get.value("artifact.directed") + + ## construct the network parts we need for the multi network with the given directedness + directed = private$determine.directedness(c("author", "artifact")) + private$network.conf$update.values(list(author.directed = directed, + artifact.directed = directed)) networks = self$get.networks() + + ## restore configured directedness + private$network.conf$update.values(list(author.directed = configured.author.directedness, + artifact.directed = configured.artifact.directedness)) + authors.to.artifacts = networks[["authors.to.artifacts"]] authors.net = networks[["authors.net"]] igraph::V(authors.net)$kind = TYPE.AUTHOR From 41cff01cf141a377c96048f0645e05fb200138e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 18 May 2025 20:54:42 +0200 Subject: [PATCH 67/92] Black-box test directedness enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-networks.R | 105 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/test-networks.R b/tests/test-networks.R index ff48676b..2eaa5c25 100644 --- a/tests/test-networks.R +++ b/tests/test-networks.R @@ -1167,3 +1167,108 @@ test_that("Get the data sources from a network with multiple relations on a sing expect_identical(expected.data.sources, get.data.sources.from.relations(network), info = "data sources: commits, mails") }) + +## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / +## Directedness ------------------------------------------------------------ + +test_that("Enforcement of directedness in sub-networks", { + + get.directedness = function(network.type, configured, relations) { + + ## configuration + proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) + net.conf = NetworkConf$new() + update = list() + update[paste0(network.type, ".directed")] = configured + update[[paste0(network.type, ".relation")]] = relations + net.conf$update.values(update) + + ## build network + network.builder = NetworkBuilder$new(project.data = ProjectData$new(project.conf = proj.conf), network.conf = net.conf) + switch(network.type, + "author" = { network = network.builder$get.author.network() }, + "artifact" = { network = network.builder$get.artifact.network() } + ) + return(igraph::is_directed(network)) + } + + assert.directedness = function(network.type, expected, configured, relations) { + actual = get.directedness(network.type, configured, relations) + info.string = paste0("network type: ", network.type, ", configured-directedness: ", configured, + ", data sources: ", paste(relations, collapse=", ")) + expect_equal(expected, actual, info=info.string) + } + + ## assume \code{ENFORCED.DIRECTEDNESS} to be empty for author and commit networks + ## and enforce undirectedness for \code{artifact.cochange} networks + + ## + ## Without enforced directedness (expected always matches configured directedness) + ## + + assert.directedness(network.type="author", expected=TRUE, configured=TRUE, relations=c("mail")) + assert.directedness(network.type="author", expected=FALSE, configured=FALSE, relations=c("mail")) + assert.directedness(network.type="author", expected=TRUE, configured=TRUE, relations=c("cochange")) + assert.directedness(network.type="author", expected=FALSE, configured=FALSE, relations=c("cochange")) + assert.directedness(network.type="author", expected=TRUE, configured=TRUE, relations=c("mail", "cochange")) + assert.directedness(network.type="author", expected=FALSE, configured=FALSE, relations=c("mail", "cochange")) + + ## + ## With enforced directedness (expected is not always configured directedness) + ## + + assert.directedness(network.type="artifact", expected=TRUE, configured=TRUE, relations=c("mail")) + assert.directedness(network.type="artifact", expected=FALSE, configured=FALSE, relations=c("mail")) + assert.directedness(network.type="artifact", expected=FALSE, configured=TRUE, relations=c("cochange")) + assert.directedness(network.type="artifact", expected=FALSE, configured=FALSE, relations=c("cochange")) + assert.directedness(network.type="artifact", expected=FALSE, configured=TRUE, relations=c("mail", "cochange")) + assert.directedness(network.type="artifact", expected=FALSE, configured=FALSE, relations=c("mail", "cochange")) + +}) + +test_that("Enforcement of directedness in multi-networks", { + + get.directedness = function(author.directed, artifact.directed, artifact.relations) { + + ## configuration + proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) + net.conf = NetworkConf$new() + net.conf$update.values(list(author.directed = author.directed, + artifact.directed = artifact.directed, + author.relation = c("cochange"), + artifact.relation = artifact.relations)) + + ## build mutli-network + network.builder = NetworkBuilder$new(project.data = ProjectData$new(project.conf = proj.conf), network.conf = net.conf) + network = network.builder$get.multi.network() + return(igraph::is_directed(network)) + } + + assert.directedness = function(expected, author.directed, artifact.directed, artifact.relations) { + actual = get.directedness(author.directed, artifact.directed, artifact.relations) + info.string = paste0("author directed: ", author.directed, ", artifact directed: ", artifact.directed, + ", artifact relation(s): ", paste(artifact.relations, collapse=", ")) + expect_equal(expected, actual, info=info.string) + } + + ## assume \code{ENFORCED.DIRECTEDNESS} to be empty for author and commit networks + ## and enforce undirectedness for \code{artifact.cochange} networks + + ## + ## Without enforced directedness from sub-networks (expect directedness of multi-network to be + ## \code{author.directedness && artifact.directedness}) + ## + + assert.directedness(expected=TRUE, author.directed=TRUE, artifact.directed=TRUE, artifact.relations=c("mail")) + assert.directedness(expected=FALSE, author.directed=TRUE, artifact.directed=FALSE, artifact.relations=c("mail")) + assert.directedness(expected=FALSE, author.directed=FALSE, artifact.directed=TRUE, artifact.relations=c("mail")) + assert.directedness(expected=FALSE, author.directed=FALSE, artifact.directed=FALSE, artifact.relations=c("mail")) + + ## + ## With enforced directedness from sub-networks (expect enforced directedness to propagate to multi-network) + ## + + assert.directedness(expected=FALSE, author.directed=TRUE, artifact.directed=TRUE, artifact.relations=c("cochange")) + assert.directedness(expected=FALSE, author.directed=TRUE, artifact.directed=TRUE, artifact.relations=c("mail", "cochange")) + +}) From 9d39c1a828399f6cb75e8f1ab938cbaa16a3220c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 22 May 2025 13:10:03 +0200 Subject: [PATCH 68/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 58e8be17..f853fe84 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,7 @@ - Reduce the amount of redundantly built networks by caching network data internally. This should improve the performance of building multi-networks, especially, when parts of the multi-networks have been built before (#119, PR #282, 64ac42aa743e7f3a724a66bcd551e5b477e30293, 1eda73265a3553e7a785a180118b1c872aeec091, beed2cc9f75619065afce1992a62ecd8ae942ce3, e2dc9954a526eb7ccb1de87571400cbbb8abb76e, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e, 231bc479f7bc04ce4048c6633bae1f1cb15307ca, a64835618f7868c93f6a0b42e196d56cedc799a7, 7537d800542d1bbdd6d3ebcc97472d58a73cbf77, 82fc4cf592c86b1a5ae161ab662433050ef5ce66, f8093033a697472679be045cabda6c1f0197b168, 9c739c0e837184f49d2fe4afb89b1eabf145d912, d6cccd7386cf0c44bfafbd684b454ab7bd1f21c2) - Internally cache commit-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e) - Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, ad3f8b3e82040a613e91a1744436a2fbdd74fe8d) +- Ensure that configured or implicitly-enforced undirectedness in partial networks is always dominant over configured or implicitly-enforced directedness. Furthermore, ensure consistency in the directedness used for edge generation and as a network attribute, especially in networks that consist of multiple partial networks such as multi-networks (PR #282, 221990654777dc866b408c9cfdf36976a0edc4b5, e8c642d09db7b11fff4f0b258c901165c0063338, f521dff2814261e1e27ef5e1a92a6cb4b9efb244, f339ee835f868fe0e7796dba3a5b1249cd5d07da) ### Fixed From 12045e55e7173b5ec0445c5ba784f684150b12be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 20 Jun 2025 22:03:17 +0200 Subject: [PATCH 69/92] Update commit hashes in 'NEWS.md' after rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index f853fe84..848a389d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,10 +13,10 @@ ### Changed/Improved - For consistency reasons: Ensure that the values of edge attributes are always lists even when they represent singular values (PR #278, 6fae1843740ed8e48c89c2ee4e61f995b5d0b8f5, 416c817998540fc0b82d9959574838b571b4d6fb) -- Reduce the amount of redundantly built networks by caching network data internally. This should improve the performance of building multi-networks, especially, when parts of the multi-networks have been built before (#119, PR #282, 64ac42aa743e7f3a724a66bcd551e5b477e30293, 1eda73265a3553e7a785a180118b1c872aeec091, beed2cc9f75619065afce1992a62ecd8ae942ce3, e2dc9954a526eb7ccb1de87571400cbbb8abb76e, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e, 231bc479f7bc04ce4048c6633bae1f1cb15307ca, a64835618f7868c93f6a0b42e196d56cedc799a7, 7537d800542d1bbdd6d3ebcc97472d58a73cbf77, 82fc4cf592c86b1a5ae161ab662433050ef5ce66, f8093033a697472679be045cabda6c1f0197b168, 9c739c0e837184f49d2fe4afb89b1eabf145d912, d6cccd7386cf0c44bfafbd684b454ab7bd1f21c2) -- Internally cache commit-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, 6c3feb9071aa8aa4915825c90d6c3a758538fc8e) -- Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, ad3f8b3e82040a613e91a1744436a2fbdd74fe8d) -- Ensure that configured or implicitly-enforced undirectedness in partial networks is always dominant over configured or implicitly-enforced directedness. Furthermore, ensure consistency in the directedness used for edge generation and as a network attribute, especially in networks that consist of multiple partial networks such as multi-networks (PR #282, 221990654777dc866b408c9cfdf36976a0edc4b5, e8c642d09db7b11fff4f0b258c901165c0063338, f521dff2814261e1e27ef5e1a92a6cb4b9efb244, f339ee835f868fe0e7796dba3a5b1249cd5d07da) +- Reduce the amount of redundantly built networks by caching network data internally. This should improve the performance of building multi-networks, especially, when parts of the multi-networks have been built before (#119, PR #282, 06a814c945f0b20af842d20247126083523cde55, 4793eab02e8792b0640fad88a90018292b1b2ab9, 8ba907fff0534c6fef39bd289ab163c90b053530, 28d22902e32e93c0d4990576da2ef3de88fdffbd, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0, b30c7f2b5b0a6d12e8024fafada5490170530ebe, 1fa340d6347090a327b4c32ece705c1f700234e5, 40cd55423be7b6521e2fc35f5aa200ff0594e77c, 8fcc74439c28b1592e964dd753bfc1cd57c062be, ca348f1de8e3b4e5786a6d2726ca14e530446896, 1d233af734f79e677d3388f7c3589ce186cc3a8d, 5dd5fc18940ce9ac9598902f175193167b471966) +- Internally cache commit-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0) +- Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) +- Ensure that configured or implicitly-enforced undirectedness in partial networks is always dominant over configured or implicitly-enforced directedness. Furthermore, ensure consistency in the directedness used for edge generation and as a network attribute, especially in networks that consist of multiple partial networks such as multi-networks (PR #282, 65ead39b7b971e5a0acbaee4e787efcf194aafc4, a776caf72256200e1bfa5578106a9b53547b00e7, 257a1c8a6a9b1c3e2a72960cc4051a87950753ee, 41cff01cf141a377c96048f0645e05fb200138e9) ### Fixed From c53389fb7bde82aa347687e26a98d23b75022530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 27 Jun 2025 13:41:54 +0200 Subject: [PATCH 70/92] Fix coverage report upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a change in Codecov's coverage report processing, uploaded reports must now contain a listing of all files that could be relevant for the report, in order to be valid. This listing is generated by 'codecov-action' from the files in the current working directory which implies that the current directory is the repository. Therefore, we must checkout the repository before triggering the 'codecov-action'. Signed-off-by: Maximilian Löffler --- .github/workflows/pull_request.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 503de3d0..5792a0c2 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -11,7 +11,7 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## Copyright 2024 by Thomas Bock ## Copyright 2025 by Leo Sendelbach ## All Rights Reserved. @@ -90,17 +90,20 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Load coverage report uses: actions/download-artifact@v4 with: name: coverage-report - name: Upload Report to CodeCov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - file: cobertura.xml + files: cobertura.xml disable_search: true + disable_telem: true fail_ci_if_error: true verbose: true - From cf4f78a633dc3a7d4ac135632ba7ec9c19540277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 1 Jul 2025 14:43:47 +0200 Subject: [PATCH 71/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 848a389d..ecae648e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,6 +17,7 @@ - Internally cache commit-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0) - Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) - Ensure that configured or implicitly-enforced undirectedness in partial networks is always dominant over configured or implicitly-enforced directedness. Furthermore, ensure consistency in the directedness used for edge generation and as a network attribute, especially in networks that consist of multiple partial networks such as multi-networks (PR #282, 65ead39b7b971e5a0acbaee4e787efcf194aafc4, a776caf72256200e1bfa5578106a9b53547b00e7, 257a1c8a6a9b1c3e2a72960cc4051a87950753ee, 41cff01cf141a377c96048f0645e05fb200138e9) +- Allow the issue data attributes `event.info.1` and `event.info.2` on network edges (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) ### Fixed From d694a6847c36d32dfee60f7d5b082e1d3ba57e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 24 Apr 2025 23:34:26 +0200 Subject: [PATCH 72/92] Fix obtaining unique dates in 'construct.edge.list.from.key.value.list' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vertices with different names may have identical associated dates, therefore, we must deduplicate both names and dates together instead of independent of each other to ensure a correct one-to-one relation of names and dates. Signed-off-by: Maximilian Löffler --- util-networks.R | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/util-networks.R b/util-networks.R index 4b3feb3b..e11cc7c3 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1558,47 +1558,49 @@ construct.edge.list.from.key.value.list = function(list, network.conf, directed edge.attributes = edge.attributes[-cols.which] } + ## construct edges if (respect.temporal.order) { ## for all subsets (sets), connect all items in there with the previous ones edge.list.data = parallel::mclapply(list, construct.edges.temporal.order, network.conf, edge.attributes, keys, keys.number, network.type) - - edge.list = plyr::rbind.fill(edge.list.data) - vertices.processed = unlist(parallel::mclapply(edge.list.data, function(data) { - return(attr(data, "vertices.processed")) - })) - } else { ## for all items in the sublists, construct the cartesian product edge.list.data = parallel::mclapply(list, construct.edges.no.temporal.order, network.conf, edge.attributes, keys, keys.number) - - edge.list = plyr::rbind.fill(edge.list.data) - vertices.processed = unlist(parallel::mclapply(edge.list.data, function(data) { - return(attr(data, "vertices.processed")) - })) - } + edge.list = plyr::rbind.fill(edge.list.data) + + ## extract names of vertices + vertex.names = unlist(parallel::mclapply(edge.list.data, function(data) { + return(attr(data, "vertices.processed")) + })) logging::logdebug("construct.edge.list.from.key.value.list: finished.") if (network.type == "commit") { - vertices.dates.processed = unlist(parallel::mclapply(edge.list.data, function(data) { - return (attr(data, "vertices.dates.processed")) + + ## extract dates of vertices + vertex.dates = unlist(parallel::mclapply(edge.list.data, function(data) { + return(attr(data, "vertices.dates.processed")) })) + + ## deduplicate vertices by name + vertices = data.frame(name = vertex.names, date = vertex.dates) + vertices = vertices[!duplicated(vertices[["name"]]), ] + return(list( vertices = data.frame( - name = unique(vertices.processed), - date = get.date.from.string(unique(vertices.dates.processed)) + name = vertices[["name"]], + date = get.date.from.string(vertices[["date"]]) ), edges = edge.list )) } else { return(list( vertices = data.frame( - name = unique(vertices.processed) + name = unique(vertex.names) ), edges = edge.list )) From 105fec1acc436378a9282c40fcae0eb0257f00be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 29 Apr 2025 14:36:46 +0200 Subject: [PATCH 73/92] Only add 'author.name' to edge attributes if it is not already present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this check, explicitly configuring 'author.name' as an 'edge.attribute' in the network configuration leads to edge lists that have an 'author.name.1' attribute in addition to the 'author.name' attribute. Signed-off-by: Maximilian Löffler --- util-networks.R | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/util-networks.R b/util-networks.R index e11cc7c3..937fa6d6 100644 --- a/util-networks.R +++ b/util-networks.R @@ -797,7 +797,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", artifact.index = match("artifact", edge.attributes, nomatch = NA) if (!is.na(artifact.index)) { edge.attributes = edge.attributes[-artifact.index] - edge.attributes = c(edge.attributes, c("author.name")) + if (!("author.name" %in% edge.attributes)) { + edge.attributes = c(edge.attributes, c("author.name")) + } } ## connect corresponding add_link and referenced_by issue-events @@ -1547,7 +1549,9 @@ construct.edge.list.from.key.value.list = function(list, network.conf, directed artifact.index = match("artifact", edge.attributes, nomatch = NA) if (!is.na(artifact.index)) { edge.attributes = edge.attributes[-artifact.index] - edge.attributes = c(edge.attributes, c("author.name")) + if (!("author.name" %in% edge.attributes)) { + edge.attributes = c(edge.attributes, c("author.name")) + } } } From bc2efd643d92da547d4677f9026540c25e730a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Thu, 15 May 2025 16:38:45 +0200 Subject: [PATCH 74/92] Add parameter to 'get.networks' to specify which networks to construct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bipartite- and the commit-network do not need to be constructed prior to constructing a multi-network. Parametrizing 'get.networks' to allow specifying which networks to construct therefore improves performance of 'get.multi.networks'. Signed-off-by: Maximilian Löffler --- util-networks.R | 59 +++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/util-networks.R b/util-networks.R index 937fa6d6..4d51e3cf 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1352,31 +1352,46 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", return(network) }, - #' Get all networks as list. - #' Build unification to avoid null-pointers. + #' Get various networks in a list. #' - #' @return all networks in a list - get.networks = function() { - logging::loginfo("Constructing all networks.") + #' @param network.type the type(s) of network(s) to be constructed + #' [default: c("author", "artifact", "commit", "bipartite", "authors.to.artifacts")] + #' + #' @return networks in a list + get.networks = function(network.type = c("author", "artifact", "commit", "bipartite", + "authors.to.artifacts")) { + + logging::loginfo("Constructing networks.") + + network.type = match.arg.or.default(network.type, several.ok = TRUE) + networks = list() - ## author-artifact relation - authors.to.artifacts = private$get.bipartite.relations() - ## bipartite network - bipartite.net = self$get.bipartite.network() ## author relation - authors.net = self$get.author.network() + if ("author" %in% network.type) { + networks[["authors.net"]] = self$get.author.network() + } + ## artifact relation - artifacts.net = self$get.artifact.network() + if ("artifact" %in% network.type) { + networks[["artifacts.net"]] = self$get.artifact.network() + } + ## commit relation - commit.net = self$get.commit.network() - - return(list( - "authors.to.artifacts" = authors.to.artifacts, - "bipartite.net" = bipartite.net, - "authors.net" = authors.net, - "artifacts.net" = artifacts.net, - "commits.net" = commit.net - )) + if ("commit" %in% network.type) { + networks[["commits.net"]] = self$get.commit.network() + } + + ## bipartite network + if ("bipartite" %in% network.type) { + networks[["bipartite.net"]] = self$get.bipartite.network() + } + + ## author-artifact relation + if ("authors.to.artifacts" %in% network.type) { + networks[["authors.to.artifacts"]] = private$get.bipartite.relations() + } + + return(networks) }, #' Get the multi network. @@ -1395,7 +1410,9 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", directed = private$determine.directedness(c("author", "artifact")) private$network.conf$update.values(list(author.directed = directed, artifact.directed = directed)) - networks = self$get.networks() + + ## construct the network parts we need for the multi network + networks = self$get.networks(network.type = c("author", "artifact", "authors.to.artifacts")) ## restore configured directedness private$network.conf$update.values(list(author.directed = configured.author.directedness, From 7dab04a5251d89c9cb286452528ef8b6775a7347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 2 Jun 2025 14:36:08 +0200 Subject: [PATCH 75/92] Adjust flattening of list attribute values in 'add.vertex.attribute' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit includes several minor fixes to the logic of flattening list values in attribute values in 'add.vertex.attribute': - To better retain POSIXct values when flattening nested lists, use do.call(base::c, ..) instead of unlist(..). - Remove a case in which an already flat list is converted into a vector as the vector will be converted in a list later-on anyways. We represent attributes as lists (see PR#274). - Rename 'list.values' parameter in 'add.vertex.attribute' and 'split.and.add.vertex.attribute' to 'flatten.values' to improve readability by reducing unnecessary negations. - Introduce missing parameter descriptions for 'list.values' (now 'flatten.values') parameter Signed-off-by: Maximilian Löffler --- util-networks-covariates.R | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/util-networks-covariates.R b/util-networks-covariates.R index 6db374a3..e57b4125 100644 --- a/util-networks-covariates.R +++ b/util-networks-covariates.R @@ -21,7 +21,7 @@ ## Copyright 2021 by Johannes Hostert ## Copyright 2022 by Niklas Schneider ## Copyright 2022 by Jonathan Baumann -## Copyright 2024 by Maximilian Löffler +## Copyright 2024-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. @@ -59,18 +59,19 @@ requireNamespace("igraph") # networks #' @param default.value The default value to add if a vertex has no matching value #' @param compute.attr The function to compute the attribute to add. Must return a named list #' with the names being the name of the vertex. +#' @param flatten.values whether to flatten lists in the attribute values [default: TRUE] #' #' @return A list of networks with the added attribute split.and.add.vertex.attribute = function(list.of.networks, project.data, attr.name, aggregation.level = c("range", "cumulative", "all.ranges", "project.cumulative", "project.all.ranges", "complete"), - default.value, compute.attr, list.attributes = FALSE) { + default.value, compute.attr, flatten.values = TRUE) { aggregation.level = match.arg.or.default(aggregation.level, default = "range") net.to.range.list = split.data.by.networks(list.of.networks, project.data, aggregation.level) - nets.with.attr = add.vertex.attribute(net.to.range.list, attr.name, default.value, compute.attr, list.attributes) + nets.with.attr = add.vertex.attribute(net.to.range.list, attr.name, default.value, compute.attr, flatten.values) return(nets.with.attr) } @@ -86,9 +87,10 @@ split.and.add.vertex.attribute = function(list.of.networks, project.data, attr.n #' @param default.value The default value to add if a vertex has no matching value #' @param compute.attr The function to compute the attribute to add. Must return a named list #' with the names being the name of the vertex. +#' @param flatten.values whether to flatten lists in the attribute values [default: TRUE] #' #' @return A list of networks with the added attribute -add.vertex.attribute = function(net.to.range.list, attr.name, default.value, compute.attr, list.attributes = FALSE) { +add.vertex.attribute = function(net.to.range.list, attr.name, default.value, compute.attr, flatten.values = TRUE) { nets.with.attr = mapply( names(net.to.range.list), net.to.range.list, @@ -123,13 +125,9 @@ add.vertex.attribute = function(net.to.range.list, attr.name, default.value, com attributes = lapply(igraph::V(current.network)$name, function(x) get.or.default(x, attrs.by.vertex.name, default.value)) - ## simplify the list of attributes to a vector if all its elements are just vectors (not lists) - if (length(attributes) > 0 && !any(sapply(attributes, is.list))) { - attributes = unlist(attributes) - } - ## otherwise, the list of attributes contains lists, so we can only remove the outermost list - else if (!list.attributes) { - attributes = unlist(attributes, recursive = FALSE) + ## flatten lists in the attribute values if specified + if (flatten.values && length(attributes) > 0) { + attributes = do.call(base::c, attributes) } net.with.attr = igraph::set_vertex_attr(current.network, attr.name, value = attributes) @@ -820,7 +818,7 @@ add.vertex.attribute.author.aggregated.activity = function(list.of.networks, pro } nets.with.attr = split.and.add.vertex.attribute(list.of.networks, project.data, name, aggregation.level, - vertex.default, compute.attr, list.attributes = TRUE) + vertex.default, compute.attr, flatten.values = FALSE) return(nets.with.attr) } From 4924ac23737dec6f915edaeb350a5cbaebbeec79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 2 Jun 2025 14:48:01 +0200 Subject: [PATCH 76/92] Remove redundant attribute conversions to POSIXct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Date-related edge attributes are supposed to be POSIXct not numeric, therefore, a conversion from unix timestamp to POSIXct must not be necessary. Signed-off-by: Maximilian Löffler --- tests/test-networks-covariates.R | 45 +------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/tests/test-networks-covariates.R b/tests/test-networks-covariates.R index 99f462b4..fea0bba5 100644 --- a/tests/test-networks-covariates.R +++ b/tests/test-networks-covariates.R @@ -22,7 +22,7 @@ ## Copyright 2021 by Johannes Hostert ## Copyright 2021-2022 by Niklas Schneider ## Copyright 2022 by Jonathan Baumann -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## All Rights Reserved. @@ -1679,10 +1679,6 @@ test_that("Test add.vertex.attribute.artifact.first.occurrence", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "first.occurrence") - - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_equal(expected.attributes[[level]], actual.attributes) }) }) @@ -1737,10 +1733,6 @@ test_that("Test add.vertex.attribute.artifact.last.edited", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "last.edited") - - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_equal(expected.attributes[[level]], actual.attributes) }) }) @@ -1911,10 +1903,6 @@ test_that("Test add.vertex.attribute.mail.thread.start.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "thread.start.date") - - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_equal(expected.attributes[[level]], actual.attributes) }) }) @@ -1963,10 +1951,6 @@ test_that("Test add.vertex.attribute.mail.thread.end.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "thread.end.date") - - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_equal(expected.attributes[[level]], actual.attributes) }) }) @@ -2410,9 +2394,6 @@ test_that("Test add.vertex.attribute.issue.opened.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "issue.opened.date") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.issues.only, actual.attributes) }) @@ -2424,9 +2405,6 @@ test_that("Test add.vertex.attribute.issue.opened.date", { type = "pull.requests") actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "pr.opened.date") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.prs.only, actual.attributes) }) @@ -2438,9 +2416,6 @@ test_that("Test add.vertex.attribute.issue.opened.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "issue.opened.date") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.both, actual.attributes) }) }) @@ -2510,9 +2485,6 @@ test_that("Test add.vertex.attribute.issue.closed.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "issue.closed.date") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.issues.only, actual.attributes) }) @@ -2524,9 +2496,6 @@ test_that("Test add.vertex.attribute.issue.closed.date", { type = "pull.requests") actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "pr.closed.date") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.prs.only, actual.attributes) }) @@ -2538,9 +2507,6 @@ test_that("Test add.vertex.attribute.issue.closed.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "issue.closed.date") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.both, actual.attributes) }) }) @@ -2653,9 +2619,6 @@ test_that("Test add.vertex.attribute.issue.last.activity.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "issue.last.activity") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.issues.only[[level]], actual.attributes) }) @@ -2667,9 +2630,6 @@ test_that("Test add.vertex.attribute.issue.last.activity.date", { type = "pull.requests") actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "pr.last.activity") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.prs.only[[level]], actual.attributes) }) @@ -2681,9 +2641,6 @@ test_that("Test add.vertex.attribute.issue.last.activity.date", { ) actual.attributes = lapply(networks.with.attr, igraph::vertex_attr, name = "issue.last.activity") - ## convert UNIX timestamps to POSIXct - actual.attributes = lapply(actual.attributes, get.date.from.unix.timestamp) - expect_identical(expected.attributes.both[[level]], actual.attributes) }) }) From aa7e3fae9f45df73054f7d9c6a96177575106c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 16 Jun 2025 17:19:13 +0200 Subject: [PATCH 77/92] Cache bipartite relations but remove them from 'get.networks' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Returning both the bipartite edge relations in addition to the constructed bipartite network from 'get.networks' is redundant. We remove the former because it is not considered a network. Additionally, we cache the bipartite relations similarly to how the author, artifact and commit network data is cached internally. Signed-off-by: Maximilian Löffler --- util-networks.R | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/util-networks.R b/util-networks.R index 4d51e3cf..ceaead09 100644 --- a/util-networks.R +++ b/util-networks.R @@ -145,6 +145,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", artifact.network.callgraph.data = NULL, commit.network.cochange.data = NULL, commit.network.commit.interaction.data = NULL, + bipartite.relations = NULL, ## * * relation-to-vertex-kind mapping ----------------------------- @@ -929,6 +930,12 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", get.bipartite.relations = function() { logging::logdebug("get.bipartite.relations: starting.") + ## do not compute anything more than once + if (!is.null(private$bipartite.relations)) { + logging::logdebug("get.bipartite.relations: finished. (already existing)") + return(private$bipartite.relations) + } + relations = private$network.conf$get.variable("artifact.relation") logging::logdebug("Using bipartite relations '%s'.", relations) @@ -944,6 +951,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", return(bip.relation) }) names(bip.relations) = relations + private$bipartite.relations = bip.relations logging::logdebug("get.bipartite.relations: finished.") return(bip.relations) @@ -994,6 +1002,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", private$artifact.network.callgraph.data = NULL private$commit.network.cochange.data = NULL private$commit.network.commit.interaction.data = NULL + private$bipartite.relations = NULL private$proj.data = private$proj.data.original if (private$network.conf$get.value("unify.date.ranges")) { private$cut.data.to.same.timestamps() @@ -1246,6 +1255,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", #' #' @return the bipartite network get.bipartite.network = function() { + ## get data by the chosen relation bipartite.relation.data = private$get.bipartite.relations() directed = private$determine.directedness("author") @@ -1349,17 +1359,17 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", } network = convert.edge.attributes.to.list(network) + return(network) }, #' Get various networks in a list. #' #' @param network.type the type(s) of network(s) to be constructed - #' [default: c("author", "artifact", "commit", "bipartite", "authors.to.artifacts")] + #' [default: c("author", "artifact", "commit", "bipartite")] #' #' @return networks in a list - get.networks = function(network.type = c("author", "artifact", "commit", "bipartite", - "authors.to.artifacts")) { + get.networks = function(network.type = c("author", "artifact", "commit", "bipartite")) { logging::loginfo("Constructing networks.") @@ -1386,11 +1396,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", networks[["bipartite.net"]] = self$get.bipartite.network() } - ## author-artifact relation - if ("authors.to.artifacts" %in% network.type) { - networks[["authors.to.artifacts"]] = private$get.bipartite.relations() - } - return(networks) }, @@ -1412,13 +1417,13 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", artifact.directed = directed)) ## construct the network parts we need for the multi network - networks = self$get.networks(network.type = c("author", "artifact", "authors.to.artifacts")) + networks = self$get.networks(network.type = c("author", "artifact")) + authors.to.artifacts = private$get.bipartite.relations() ## restore configured directedness private$network.conf$update.values(list(author.directed = configured.author.directedness, artifact.directed = configured.artifact.directedness)) - authors.to.artifacts = networks[["authors.to.artifacts"]] authors.net = networks[["authors.net"]] igraph::V(authors.net)$kind = TYPE.AUTHOR artifacts.net = networks[["artifacts.net"]] From e9a0c1681a0b63bd831fe7657ce25a9d50dc6e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 4 Jul 2025 23:33:56 +0200 Subject: [PATCH 78/92] Add tests for 'network.type' parameter of 'get.networks' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-networks.R | 64 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test-networks.R b/tests/test-networks.R index 2eaa5c25..d393223d 100644 --- a/tests/test-networks.R +++ b/tests/test-networks.R @@ -1272,3 +1272,67 @@ test_that("Enforcement of directedness in multi-networks", { assert.directedness(expected=FALSE, author.directed=TRUE, artifact.directed=TRUE, artifact.relations=c("mail", "cochange")) }) + +## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / +## Constructing multiple networks ------------------------------------------ + +test_that("Construct multiple different networks at once", { + + ## configurations + proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) + proj.data = ProjectData$new(project.conf = proj.conf) + net.conf = NetworkConf$new() + net.conf$update.value("commit.relation", "commit.interaction") + + ## construct network builder + network.builder = NetworkBuilder$new(project.data = proj.data, network.conf = net.conf) + + # available network types + available.network.types = c("author", "artifact", "commit", "bipartite") + network.type.map = list( + author = "authors.net", + artifact = "artifacts.net", + commit = "commits.net", + bipartite = "bipartite.net" + ) + + assert.correct.networks = function(network.builder, network.type) { + + if (is.null(network.type)) { + # get all available networks + networks = network.builder$get.networks() + network.type = available.network.types + + } else { + # get specified networks + networks = network.builder$get.networks(network.type = network.type) + } + + expected.network.types = unname(sapply(network.type, function(type) network.type.map[[type]])) + actual.network.types = names(networks) + + expect_identical(expected.network.types, actual.network.types, + info = paste0("all networks (", + paste(names(networks), collapse=", "), + ") are of the requested type (", + paste(expected.network.types, collapse = ", "), + ")")) + } + + # get all subset combinations of an input list + get.subsets = function(input.list) { + output.list = list(NULL) + for (i in seq_along(input.list)) { + sublist = combn(input.list, i, simplify = FALSE) + output.list = c(output.list, sublist) + } + return(output.list) + } + + # test all combinations of network types + for (network.type in get.subsets(available.network.types)) { + assert.correct.networks(network.builder, network.type = network.type) + } + +}) + From d5e1e4801230224e6272cd66873bd43bb7f04a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Fri, 4 Jul 2025 23:40:22 +0200 Subject: [PATCH 79/92] Handle empty edges in 'get.commit.network.commit.interactions' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-networks.R | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/util-networks.R b/util-networks.R index ceaead09..c5b0f732 100644 --- a/util-networks.R +++ b/util-networks.R @@ -858,11 +858,13 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## set the commits as the 'to' and 'from' of the network and order the dataframe edges = edges[, c("base.hash", "commit.hash", "func", "interacting.author", "file", "base.author", "base.func", "base.file")] - if (nrow(edges) > 0) { - edges[["artifact.type"]] = ARTIFACT.COMMIT.INTERACTION + if (!is.null(edges)) { + if (nrow(edges) > 0) { + edges[["artifact.type"]] = ARTIFACT.COMMIT.INTERACTION + } + colnames(edges)[1] = "to" + colnames(edges)[2] = "from" } - colnames(edges)[1] = "to" - colnames(edges)[2] = "from" ## construct network data network.data = private$construct.network.data( From 7928736ce56a868b519ebac4282ae0a2305006ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Wed, 2 Jul 2025 15:54:42 +0200 Subject: [PATCH 80/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index ecae648e..81cbebf2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,13 +14,20 @@ - For consistency reasons: Ensure that the values of edge attributes are always lists even when they represent singular values (PR #278, 6fae1843740ed8e48c89c2ee4e61f995b5d0b8f5, 416c817998540fc0b82d9959574838b571b4d6fb) - Reduce the amount of redundantly built networks by caching network data internally. This should improve the performance of building multi-networks, especially, when parts of the multi-networks have been built before (#119, PR #282, 06a814c945f0b20af842d20247126083523cde55, 4793eab02e8792b0640fad88a90018292b1b2ab9, 8ba907fff0534c6fef39bd289ab163c90b053530, 28d22902e32e93c0d4990576da2ef3de88fdffbd, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0, b30c7f2b5b0a6d12e8024fafada5490170530ebe, 1fa340d6347090a327b4c32ece705c1f700234e5, 40cd55423be7b6521e2fc35f5aa200ff0594e77c, 8fcc74439c28b1592e964dd753bfc1cd57c062be, ca348f1de8e3b4e5786a6d2726ca14e530446896, 1d233af734f79e677d3388f7c3589ce186cc3a8d, 5dd5fc18940ce9ac9598902f175193167b471966) -- Internally cache commit-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0) +- Internally cache commit-network data and bipartite-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, PR #285, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0, aa7e3fae9f45df73054f7d9c6a96177575106c7d) - Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) - Ensure that configured or implicitly-enforced undirectedness in partial networks is always dominant over configured or implicitly-enforced directedness. Furthermore, ensure consistency in the directedness used for edge generation and as a network attribute, especially in networks that consist of multiple partial networks such as multi-networks (PR #282, 65ead39b7b971e5a0acbaee4e787efcf194aafc4, a776caf72256200e1bfa5578106a9b53547b00e7, 257a1c8a6a9b1c3e2a72960cc4051a87950753ee, 41cff01cf141a377c96048f0645e05fb200138e9) - Allow the issue data attributes `event.info.1` and `event.info.2` on network edges (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) +- Add a `network.type` parameter to `get.networks` in which the caller can specify the types of networks to be constructed. This improves performance in cases where not all network types are needed, such as when building multi-networks (PR #285, bc2efd643d92da547d4677f9026540c25e730a03, e9a0c1681a0b63bd831fe7657ce25a9d50dc6e83) +- Rename the `list.attributes` parameter in `add.vertex.attribute` and `split.and.add.vertex.attribute` to `flatten.values` with inverted semantics and introduce documentation for it to improve comprehensibility (PR #285, 7dab04a5251d89c9cb286452528ef8b6775a7347) ### Fixed +- Fix a bug in `construct.edge.list.from.key.value.list` that could cause a crash when constructing a network where different vertices have identical associated timestamps (PR #285, d694a6847c36d32dfee60f7d5b082e1d3ba57e01) +- Fix a bug in network construction that could lead to edges having an unwanted `author.name.1` attribute (PR #285, 105fec1acc436378a9282c40fcae0eb0257f00be) +- Ensure that POSIXct values are correctly handled in `add.vertex.attribute`, i.e., that they are not converted to numeric values (PR #285, 7dab04a5251d89c9cb286452528ef8b6775a7347, 4924ac23737dec6f915edaeb350a5cbaebbeec79) +- Handle empty edges when constructing commit networks using commit-interaction data (PR #285, d5e1e4801230224e6272cd66873bd43bb7f04a00) + ## 5.0 ### Announcement From 7481099af109e1897b9e5754beb1c7da9f39ffb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 17 Jun 2025 15:00:04 +0200 Subject: [PATCH 81/92] Ensure that commit ids are unique between proximity and feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- .../testing/test_feature/feature/002--v2-v3/commits.list | 4 ++-- .../results/testing/test_feature/feature/commits.list | 4 ++-- tests/test-data.R | 4 ++-- tests/test-read.R | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list b/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list index 35f41651..ccc9f22f 100644 --- a/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list +++ b/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list @@ -1,7 +1,7 @@ 32710;"2016-07-12 16:05:41";"Olaf";"olaf@example.org";"2016-07-12 17:05:55";"Thomas";"thomas@example.org";"3a0ed78458b3976243db6829f63eba3eead26774";1;1;0;1;"test2.c";"Base_Feature";"Feature";1 32710;"2016-07-12 16:05:41";"Olaf";"olaf@example.org";"2016-07-12 17:05:55";"Thomas";"thomas@example.org";"3a0ed78458b3976243db6829f63eba3eead26774";1;1;0;1;"test2.c";"Base_Feature";"FeatureExpression";1 32714;"2016-07-12 16:06:10";"Karl";"karl@example.org";"2016-07-12 16:06:10";"Karl";"karl@example.org";"1143db502761379c2bfcecc2007fc34282e7ee61";1;1;0;1;"test3.c";"Base_Feature";"Feature";1 -32715;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 -32716;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 +32707;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 +32709;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"Base_Feature";"Feature";1 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"foo";"Feature";1 diff --git a/tests/codeface-data/results/testing/test_feature/feature/commits.list b/tests/codeface-data/results/testing/test_feature/feature/commits.list index 2f1476d0..a30e44cb 100644 --- a/tests/codeface-data/results/testing/test_feature/feature/commits.list +++ b/tests/codeface-data/results/testing/test_feature/feature/commits.list @@ -5,8 +5,8 @@ 32710;"2016-07-12 16:05:41";"Olaf";"olaf@example.org";"2016-07-12 17:05:55";"Thomas";"thomas@example.org";"3a0ed78458b3976243db6829f63eba3eead26774";1;1;0;1;"test2.c";"Base_Feature";"Feature";1 32710;"2016-07-12 16:05:41";"Olaf";"olaf@example.org";"2016-07-12 17:05:55";"Thomas";"thomas@example.org";"3a0ed78458b3976243db6829f63eba3eead26774";1;1;0;1;"test2.c";"Base_Feature";"FeatureExpression";1 32714;"2016-07-12 16:06:10";"Karl";"karl@example.org";"2016-07-12 16:06:10";"Karl";"karl@example.org";"1143db502761379c2bfcecc2007fc34282e7ee61";1;1;0;1;"test3.c";"Base_Feature";"Feature";1 -32715;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 -32716;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 +32707;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 +32709;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"Base_Feature";"Feature";1 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"foo";"Feature";1 31711;"2016-07-12 16:06:33";"Thomas";"thomas@example.org";"2016-07-12 16:06:33";"";"thomas@example.org";"2ef7bde608ce5404e97d5f042f95f89f1c232871";1;1;0;1;"test2.c";"foo";"Feature";1 diff --git a/tests/test-data.R b/tests/test-data.R index c983946d..fed1cc61 100644 --- a/tests/test-data.R +++ b/tests/test-data.R @@ -306,7 +306,7 @@ test_that("Merge commit messages to commit data", { commits = proj.data$get.commits.unfiltered() - commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32715, 32716, + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32707, 32709, 32711, 32711)), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", @@ -349,7 +349,7 @@ test_that("Merge commit message titles to commit data", { commits = proj.data$get.commits.unfiltered() - commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32715, 32716, + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32707, 32709, 32711, 32711)), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", diff --git a/tests/test-read.R b/tests/test-read.R index f01d16c1..bf89594f 100644 --- a/tests/test-read.R +++ b/tests/test-read.R @@ -49,7 +49,7 @@ test_that("Read the raw commit data with the feature artifact.", { commit.data.read = read.commits(proj.conf$get.value("datapath"), proj.conf$get.value("artifact")) ## build the expected data.frame - commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32712, 32713, 32713, 32710, 32710, 32714, 32715, 32716, + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32712, 32713, 32713, 32710, 32710, 32714, 32707, 32709, 32711, 32711)), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:05:41", From 3e53285426010cf7bf48fa23daa484f29f80ac78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 17 Jun 2025 15:12:42 +0200 Subject: [PATCH 82/92] Update codeface data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are two interesting cases that the codeface data was missing before. 1) Commits that touch multiple different files / functions, 2) Commits by different authors that are issued at the exact same time. This works towards fixing #284. Signed-off-by: Maximilian Löffler --- .../feature/002--v2-v3/commits.list | 2 + .../testing/test_feature/feature/commits.list | 2 + .../proximity/002--v2-v3/commits.list | 2 + .../test_proximity/proximity/commits.list | 2 + tests/test-core-peripheral.R | 7 +- tests/test-data.R | 102 ++++++----- tests/test-networks-artifact.R | 10 +- tests/test-networks-author.R | 29 +-- tests/test-networks-bipartite.R | 92 ++++++---- tests/test-networks-commit.R | 142 +++++++++------ tests/test-networks-covariates.R | 98 +++++----- tests/test-networks-multi-relation.R | 169 +++++++++--------- tests/test-networks-multi.R | 112 ++++++------ tests/test-read.R | 104 ++++++----- tests/test-split-data-activity-based.R | 116 ++++++------ tests/test-split-data-time-based.R | 10 +- 16 files changed, 552 insertions(+), 447 deletions(-) diff --git a/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list b/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list index ccc9f22f..6b2db060 100644 --- a/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list +++ b/tests/codeface-data/results/testing/test_feature/feature/002--v2-v3/commits.list @@ -2,6 +2,8 @@ 32710;"2016-07-12 16:05:41";"Olaf";"olaf@example.org";"2016-07-12 17:05:55";"Thomas";"thomas@example.org";"3a0ed78458b3976243db6829f63eba3eead26774";1;1;0;1;"test2.c";"Base_Feature";"FeatureExpression";1 32714;"2016-07-12 16:06:10";"Karl";"karl@example.org";"2016-07-12 16:06:10";"Karl";"karl@example.org";"1143db502761379c2bfcecc2007fc34282e7ee61";1;1;0;1;"test3.c";"Base_Feature";"Feature";1 32707;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 +32708;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test2.c";"foo";"Feature";1 +32708;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test3.c";"foo";"Feature";2 32709;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"Base_Feature";"Feature";1 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"foo";"Feature";1 diff --git a/tests/codeface-data/results/testing/test_feature/feature/commits.list b/tests/codeface-data/results/testing/test_feature/feature/commits.list index a30e44cb..040901a4 100644 --- a/tests/codeface-data/results/testing/test_feature/feature/commits.list +++ b/tests/codeface-data/results/testing/test_feature/feature/commits.list @@ -6,6 +6,8 @@ 32710;"2016-07-12 16:05:41";"Olaf";"olaf@example.org";"2016-07-12 17:05:55";"Thomas";"thomas@example.org";"3a0ed78458b3976243db6829f63eba3eead26774";1;1;0;1;"test2.c";"Base_Feature";"FeatureExpression";1 32714;"2016-07-12 16:06:10";"Karl";"karl@example.org";"2016-07-12 16:06:10";"Karl";"karl@example.org";"1143db502761379c2bfcecc2007fc34282e7ee61";1;1;0;1;"test3.c";"Base_Feature";"Feature";1 32707;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 +32708;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test2.c";"foo";"Feature";1 +32708;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test3.c";"foo";"Feature";2 32709;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"Base_Feature";"Feature";1 32711;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"foo";"Feature";1 diff --git a/tests/codeface-data/results/testing/test_proximity/proximity/002--v2-v3/commits.list b/tests/codeface-data/results/testing/test_proximity/proximity/002--v2-v3/commits.list index bbf2e3b6..25ba4fbb 100644 --- a/tests/codeface-data/results/testing/test_proximity/proximity/002--v2-v3/commits.list +++ b/tests/codeface-data/results/testing/test_proximity/proximity/002--v2-v3/commits.list @@ -2,4 +2,6 @@ 32719;"2016-07-12 16:06:10";"Karl";"karl@example.org";"2016-07-12 16:06:10";"Karl";"karl@example.org";"1143db502761379c2bfcecc2007fc34282e7ee61";1;1;0;1;"test3.c";"test_function";"Function";1 32715;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"File_Level";"Function";1 32720;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 +32722;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test2.c";"test_function";"Function";1 +32722;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test3.c";"test_function";"Function";2 32721;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 diff --git a/tests/codeface-data/results/testing/test_proximity/proximity/commits.list b/tests/codeface-data/results/testing/test_proximity/proximity/commits.list index e4f136f1..00a41097 100644 --- a/tests/codeface-data/results/testing/test_proximity/proximity/commits.list +++ b/tests/codeface-data/results/testing/test_proximity/proximity/commits.list @@ -4,6 +4,8 @@ 32719;"2016-07-12 16:06:10";"Karl";"karl@example.org";"2016-07-12 16:06:10";"Karl";"karl@example.org";"1143db502761379c2bfcecc2007fc34282e7ee61";1;1;0;1;"test3.c";"test_function";"Function";1 32715;"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"2016-07-12 16:06:32";"Thomas";"thomas@example.org";"0a1a5c523d835459c42f33e863623138555e2526";1;1;0;1;"test2.c";"File_Level";"Function";1 32720;"2016-07-12 16:06:20";"Karl";"karl@example.org";"2016-07-12 16:06:20";"Karl";"karl@example.org";"418d1dc4929ad1df251d2aeb833dd45757b04a6f";1;1;0;1;"";"";"";0 +32722;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test2.c";"test_function";"Function";1 +32722;"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"2016-07-12 16:06:20";"Thomas";"thomas@example.org";"7d5219c4ba15b8962203f0ae37f9854167914915";2;3;1;2;"test3.c";"test_function";"Function";2 32721;"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"2016-07-12 16:06:30";"Thomas";"thomas@example.org";"d01921773fae4bed8186b0aa411d6a2f7a6626e6";1;1;0;1;"";"";"";0 32811;"2016-07-12 16:06:33";"Thomas";"thomas@example.org";"2016-07-12 16:06:33";"";"thomas@example.org";"2ef7bde608ce5404e97d5f042f95f89f1c232871";1;1;0;1;"test2.c";"foo";"Feature";1 32911;"2016-07-12 16:06:34";"Thomas";"thomas@example.org";"2016-07-12 16:06:34";"deleted user";"thomas@example.org";"c6954cb75e3eeec5b827f64e97b6a4ba187c0d55";1;1;0;1;"test2.c";"foo";"Feature";1 diff --git a/tests/test-core-peripheral.R b/tests/test-core-peripheral.R index a027c356..06e9cc1a 100644 --- a/tests/test-core-peripheral.R +++ b/tests/test-core-peripheral.R @@ -248,7 +248,7 @@ test_that("Commit-count classification using 'result.limit'" , { result = get.author.class.commit.count(proj.data, result.limit = 3) ## Assert - expected.core = data.frame(author.name = c("Björn", "Olaf", "Thomas"), commit.count = c(1, 1, 1)) + expected.core = data.frame(author.name = c("Thomas", "Björn", "Olaf"), commit.count = c(2, 1, 1)) expected = list(core = expected.core, peripheral = expected.core[0, ]) row.names(result[["core"]]) = NULL @@ -262,8 +262,9 @@ test_that("LOC-count classification" , { result = get.author.class.loc.count(proj.data) ## Assert - expected.core = data.frame(author.name = c("Björn", "Olaf", "Thomas"), loc.count = c(2, 1, 1)) - expected = list(core = expected.core, peripheral = expected.core[0, ]) + expected.core = data.frame(author.name = c("Thomas", "Björn"), loc.count = c(5, 2)) + expected.peripheral = data.frame(author.name = c("Olaf"), loc.count = c(1)) + expected = list(core = expected.core, peripheral = expected.peripheral) row.names(result[["core"]]) = NULL row.names(result[["peripheral"]]) = NULL diff --git a/tests/test-data.R b/tests/test-data.R index fed1cc61..455d2fea 100644 --- a/tests/test-data.R +++ b/tests/test-data.R @@ -306,37 +306,45 @@ test_that("Merge commit messages to commit data", { commits = proj.data$get.commits.unfiltered() - commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32707, 32709, - 32711, 32711)), + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32707, 32708, + 32708, 32709, 32711, 32711)), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - author.name = c("Björn", "Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas"), + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", + "2016-07-12 16:06:32")), + author.name = c("Björn", "Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", "Thomas"), author.email = c("bjoern@example.org", "olaf@example.org", "olaf@example.org", "karl@example.org", - "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org"), + "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org", "thomas@example.org"), committer.date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-20 10:00:44", "2016-07-12 17:05:55", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - committer.name = c("Björn", "Björn", "Thomas", "Karl", "Karl", "Thomas", "Thomas", "Thomas"), + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", + "2016-07-12 16:06:32")), + committer.name = c("Björn", "Björn", "Thomas", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", "Thomas"), committer.email = c("bjoern@example.org", "bjoern@example.org", "thomas@example.org", "karl@example.org", - "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org"), + "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org", "thomas@example.org"), hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", - "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "7d5219c4ba15b8962203f0ae37f9854167914915", + "7d5219c4ba15b8962203f0ae37f9854167914915", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - changed.files = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1)), - added.lines = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1)), - deleted.lines = as.integer(c(1, 0, 0, 0, 0, 0, 0, 0)), - diff.size = as.integer(c(2, 1, 1, 1, 1, 1, 1, 1)), + changed.files = as.integer(c(1, 1, 1, 1, 1, 2, 2, 1, 1, 1)), + added.lines = as.integer(c(1, 1, 1, 1, 1, 3, 3, 1, 1, 1)), + deleted.lines = as.integer(c(1, 0, 0, 0, 0, 1, 1, 0, 0, 0)), + diff.size = as.integer(c(2, 1, 1, 1, 1, 2, 2, 1, 1, 1)), file = c("test.c", "test.c", "test2.c", "test3.c", UNTRACKED.FILE, - UNTRACKED.FILE, "test2.c", "test2.c"), - artifact = c("A", "A", "Base_Feature", "Base_Feature", - UNTRACKED.FILE.EMPTY.ARTIFACT, UNTRACKED.FILE.EMPTY.ARTIFACT, "Base_Feature", "foo"), + "test2.c", "test3.c", UNTRACKED.FILE, "test2.c", "test2.c"), + artifact = c("A", "A", "Base_Feature", "Base_Feature", UNTRACKED.FILE.EMPTY.ARTIFACT, + "foo", "foo", UNTRACKED.FILE.EMPTY.ARTIFACT, "Base_Feature", "foo"), artifact.type = c("Feature", "Feature", "Feature","Feature", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, - UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "Feature", "Feature"), - artifact.diff.size = as.integer(c(1, 1, 1, 1, 0, 0, 1, 1)), - title = c("Add stuff", "Add some more stuff", "I added important things", "I wish it would work now", "Wish", "...", "", ""), - message = c("", "", "the things are\nnothing", "", "intensifies", "still\ndoesn't\nwork\nas expected", "", "")) + "Feature", "Feature", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "Feature", "Feature"), + artifact.diff.size = as.integer(c(1, 1, 1, 1, 0, 1, 2, 0, 1, 1)), + title = c("Add stuff", "Add some more stuff", "I added important things", "I wish it would work now", NA, NA, NA, NA, "", ""), + message = c("", "", "the things are\nnothing", "", NA, NA, NA, NA, "", "")) + + commits = remove.row.names.from.data(commits) + commit.data.expected = remove.row.names.from.data(commit.data.expected) expect_identical(commits, commit.data.expected, info = "Add commit messages with title") }) @@ -349,36 +357,44 @@ test_that("Merge commit message titles to commit data", { commits = proj.data$get.commits.unfiltered() - commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32707, 32709, - 32711, 32711)), + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32707, 32708, + 32708, 32709, 32711, 32711)), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - author.name = c("Björn", "Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas"), + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", + "2016-07-12 16:06:32")), + author.name = c("Björn", "Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", "Thomas"), author.email = c("bjoern@example.org", "olaf@example.org", "olaf@example.org", "karl@example.org", - "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org"), + "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org", "thomas@example.org"), committer.date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-20 10:00:44", "2016-07-12 17:05:55", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - committer.name = c("Björn", "Björn", "Thomas", "Karl", "Karl", "Thomas", "Thomas", "Thomas"), + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", + "2016-07-12 16:06:32")), + committer.name = c("Björn", "Björn", "Thomas", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", "Thomas"), committer.email = c("bjoern@example.org", "bjoern@example.org", "thomas@example.org", "karl@example.org", - "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org"), + "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org", "thomas@example.org"), hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", - "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "7d5219c4ba15b8962203f0ae37f9854167914915", + "7d5219c4ba15b8962203f0ae37f9854167914915", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - changed.files = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1)), - added.lines = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1)), - deleted.lines = as.integer(c(1, 0, 0, 0, 0, 0, 0, 0)), - diff.size = as.integer(c(2, 1, 1, 1, 1, 1, 1, 1)), + changed.files = as.integer(c(1, 1, 1, 1, 1, 2, 2, 1, 1, 1)), + added.lines = as.integer(c(1, 1, 1, 1, 1, 3, 3, 1, 1, 1)), + deleted.lines = as.integer(c(1, 0, 0, 0, 0, 1, 1, 0, 0, 0)), + diff.size = as.integer(c(2, 1, 1, 1, 1, 2, 2, 1, 1, 1)), file = c("test.c", "test.c", "test2.c", "test3.c", UNTRACKED.FILE, - UNTRACKED.FILE, "test2.c", "test2.c"), - artifact = c("A", "A", "Base_Feature", "Base_Feature", - UNTRACKED.FILE.EMPTY.ARTIFACT, UNTRACKED.FILE.EMPTY.ARTIFACT, "Base_Feature", "foo"), + "test2.c", "test3.c", UNTRACKED.FILE, "test2.c", "test2.c"), + artifact = c("A", "A", "Base_Feature", "Base_Feature", UNTRACKED.FILE.EMPTY.ARTIFACT, + "foo", "foo", UNTRACKED.FILE.EMPTY.ARTIFACT, "Base_Feature", "foo"), artifact.type = c("Feature", "Feature", "Feature","Feature", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, - UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "Feature", "Feature"), - artifact.diff.size = as.integer(c(1, 1, 1, 1, 0, 0, 1, 1)), - title = c("Add stuff", "Add some more stuff", "I added important things", "I wish it would work now", "Wish", "...", "", "")) + "Feature", "Feature", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "Feature", "Feature"), + artifact.diff.size = as.integer(c(1, 1, 1, 1, 0, 1, 2, 0, 1, 1)), + title = c("Add stuff", "Add some more stuff", "I added important things", "I wish it would work now", NA, NA, NA, NA, "", "")) + + commits = remove.row.names.from.data(commits) + commit.data.expected = remove.row.names.from.data(commit.data.expected) expect_identical(commits, commit.data.expected, info = "Add only commit title") }) diff --git a/tests/test-networks-artifact.R b/tests/test-networks-artifact.R index 56300d5a..d21ecada 100644 --- a/tests/test-networks-artifact.R +++ b/tests/test-networks-artifact.R @@ -44,12 +44,12 @@ test_that("Network construction of the undirected artifact-cochange network", { type = TYPE.ARTIFACT) ## 2) edges edges = data.frame( - from = "Base_Feature", - to = "foo", - date = get.date.from.string("2016-07-12 16:06:32"), + from = c("Base_Feature", "foo"), + to = c("foo", "foo"), + date = get.date.from.string(c("2016-07-12 16:06:32", "2016-07-12 16:06:20")), artifact.type = "Feature", - hash = "0a1a5c523d835459c42f33e863623138555e2526", - file = "test2.c", + hash = c("0a1a5c523d835459c42f33e863623138555e2526", "7d5219c4ba15b8962203f0ae37f9854167914915"), + file = c("test2.c", "test3.c"), author.name = "Thomas", weight = 1, type = TYPE.EDGES.INTRA, diff --git a/tests/test-networks-author.R b/tests/test-networks-author.R index 6e7172dc..d0a93b59 100644 --- a/tests/test-networks-author.R +++ b/tests/test-networks-author.R @@ -301,15 +301,19 @@ test_that("Network construction of the undirected but temorally ordered author-c type = TYPE.AUTHOR) ## edge attributes - data = data.frame(comb.1. = c("Olaf", "Karl", "Thomas", "Thomas"), - comb.2. = c("Björn", "Olaf", "Olaf", "Karl"), + data = data.frame(comb.1. = c("Olaf", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", "Thomas"), + comb.2. = c("Björn", "Olaf", "Olaf", "Karl", "Thomas", "Thomas", "Thomas"), date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", - "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20", "2016-07-12 16:06:32", + "2016-07-12 16:06:32")), artifact.type = "Feature", hash = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test2.c", "test2.c"), - artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature"), + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", + "0a1a5c523d835459c42f33e863623138555e2526"), + file = c("test.c", "test3.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c"), + artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature", "foo", "foo", "foo"), weight = 1, type = TYPE.EDGES.INTRA, relation = "cochange" @@ -343,15 +347,18 @@ test_that("Network construction of the directed author-cochange network", { type = TYPE.AUTHOR) ## edge attributes - data = data.frame(from = c("Olaf", "Karl", "Thomas", "Thomas"), - to = c("Björn", "Olaf", "Olaf", "Karl"), + data = data.frame(from = c("Olaf", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", "Thomas"), + to = c("Björn", "Olaf", "Olaf", "Karl", "Thomas", "Thomas", "Thomas"), date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", "2016-07-12 16:06:32", + "2016-07-12 16:06:32", "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), artifact.type = "Feature", hash = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test2.c", "test2.c"), - artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature"), + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", + "0a1a5c523d835459c42f33e863623138555e2526"), + file = c("test.c", "test3.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c"), + artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature", "foo", "foo", "foo"), weight = 1, type = TYPE.EDGES.INTRA, relation = "cochange" diff --git a/tests/test-networks-bipartite.R b/tests/test-networks-bipartite.R index 3c6fd3b4..796a66ed 100644 --- a/tests/test-networks-bipartite.R +++ b/tests/test-networks-bipartite.R @@ -67,16 +67,18 @@ test_that("Construction of the bipartite network for the feature artifact with a vertices = plyr::rbind.fill(authors, artifacts) ## 2) construct expected edge attributes network.expected.data = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas"), - to = c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo"), + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas", "Thomas"), + to = c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - artifact.type = c("Feature", "Feature", "Feature", "Feature", "Feature", "Feature"), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + artifact.type = "Feature", hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c"), - artifact = c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo"), + file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c"), + artifact = c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo"), weight = 1, type = TYPE.EDGES.INTER, relation = "cochange" @@ -121,16 +123,18 @@ test_that("Construction of the bipartite network for the file artifact with auth vertices = plyr::rbind.fill(authors, artifacts) ## 2) construct expected edge attributes network.expected.data = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas"), - to = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas"), + to = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32")), - artifact.type = c("File", "File", "File", "File", "File"), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32")), + artifact.type = "File", hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), - artifact = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), + file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), + artifact = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), weight = 1, type = TYPE.EDGES.INTER, relation = "cochange" @@ -168,23 +172,27 @@ test_that("Construction of the bipartite network for the function artifact with type = TYPE.AUTHOR ) artifacts = data.frame( - name = c("File_Level", "test3.c::test_function"), + name = c("File_Level", "test3.c::test_function", "test2.c::test_function"), kind = "Function", type = TYPE.ARTIFACT ) vertices = plyr::rbind.fill(authors, artifacts) ## 2) construct expected edge attributes network.expected.data = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas"), - to = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", "File_Level"), + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas"), + to = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", + "test2.c::test_function", "test3.c::test_function", "File_Level"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32")), - artifact.type = c("Function", "Function", "Function", "Function", "Function"), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32")), + artifact.type = "Function", hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), - artifact = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", "File_Level"), + file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), + artifact = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", + "test2.c::test_function", "test3.c::test_function", "File_Level"), weight = 1, type = TYPE.EDGES.INTER, relation = "cochange" @@ -350,16 +358,18 @@ test_that("Construction of the directed bipartite network for the feature artifa vertices = plyr::rbind.fill(authors, artifacts) ## 2) construct expected edge attributes network.expected.data = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas"), - to = c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo"), + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas", "Thomas"), + to = c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - artifact.type = c("Feature", "Feature", "Feature", "Feature", "Feature", "Feature"), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + artifact.type = "Feature", hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c"), - artifact = c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo"), + file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c"), + artifact = c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo"), weight = 1, type = TYPE.EDGES.INTER, relation = "cochange" @@ -404,16 +414,18 @@ test_that("Construction of the directed bipartite network for the file artifact vertices = plyr::rbind.fill(authors, artifacts) ## 2) construct expected edge attributes network.expected.data = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas"), - to = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas"), + to = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32")), - artifact.type = c("File", "File", "File", "File", "File"), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32")), + artifact.type = "File", hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), - artifact = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), + file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), + artifact = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), weight = 1, type = TYPE.EDGES.INTER, relation = "cochange" @@ -452,23 +464,27 @@ test_that("Construction of the directed bipartite network for the function artif type = TYPE.AUTHOR ) artifacts = data.frame( - name = c("File_Level", "test3.c::test_function"), + name = c("File_Level", "test3.c::test_function", "test2.c::test_function"), kind = "Function", type = TYPE.ARTIFACT ) vertices = plyr::rbind.fill(authors, artifacts) ## 2) construct expected edge attributes network.expected.data = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas"), - to = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", "File_Level"), + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas"), + to = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", + "test2.c::test_function", "test3.c::test_function", "File_Level"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32")), - artifact.type = c("Function", "Function", "Function", "Function", "Function"), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32")), + artifact.type = "Function", hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c"), - artifact = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", "File_Level"), + file = c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c"), + artifact = c("File_Level", "test3.c::test_function", "File_Level", "File_Level", + "test2.c::test_function", "test3.c::test_function", "File_Level"), weight = 1, type = TYPE.EDGES.INTER, relation = "cochange" diff --git a/tests/test-networks-commit.R b/tests/test-networks-commit.R index e5c39672..cff8849d 100644 --- a/tests/test-networks-commit.R +++ b/tests/test-networks-commit.R @@ -109,29 +109,37 @@ patrick::with_parameters_test_that("Network construction with cochange as relati name = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", "1143db502761379c2bfcecc2007fc34282e7ee61"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", + "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:10")), kind = TYPE.COMMIT, type = TYPE.COMMIT - ) + ) edges = data.frame( - from = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "3a0ed78458b3976243db6829f63eba3eead26774"), - to = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "0a1a5c523d835459c42f33e863623138555e2526"), - date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:32")), - artifact.type = c("File", "File"), - artifact = c("test.c", "test2.c"), - weight = c(1, 1), - type = c(TYPE.EDGES.INTRA, TYPE.EDGES.INTRA), - relation = c("cochange", "cochange") - ) + from = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "3a0ed78458b3976243db6829f63eba3eead26774", + "3a0ed78458b3976243db6829f63eba3eead26774", "7d5219c4ba15b8962203f0ae37f9854167914915", + "1143db502761379c2bfcecc2007fc34282e7ee61"), + to = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "7d5219c4ba15b8962203f0ae37f9854167914915", + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915"), + date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:20", + "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20")), + artifact.type = "File", + artifact = c("test.c", "test2.c", "test2.c", "test2.c", "test3.c"), + weight = 1, + type = TYPE.EDGES.INTRA, + relation = "cochange" + ) if (test.directed) { - edges <- edges[, c(2, 1, 3, 4, 5, 6, 7, 8), ] + edges = edges[, c(2, 1, 3, 4, 5, 6, 7, 8), ] } network = igraph::graph_from_data_frame(edges, directed = test.directed, vertices = vertices) network = convert.edge.attributes.to.list(network) @@ -160,11 +168,13 @@ patrick::with_parameters_test_that("Network construction with cochange as relati "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915", "1143db502761379c2bfcecc2007fc34282e7ee61"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:06:32", + "2016-07-12 16:06:20", "2016-07-12 16:06:10")), kind = TYPE.COMMIT, type = TYPE.COMMIT @@ -172,18 +182,21 @@ patrick::with_parameters_test_that("Network construction with cochange as relati edges = data.frame( from = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", - "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774"), + "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61"), to = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", - "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915"), date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:05:41", - "2016-07-12 16:06:32", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - artifact.type = c("Function", "Function", "Function", "Function", "Function", "Function"), - artifact = c("File_Level", "File_Level", "File_Level", "File_Level", "File_Level", "File_Level"), - weight = c(1, 1, 1, 1, 1, 1), - type = c(TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, - TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA), - relation = c("cochange", "cochange", "cochange", "cochange", "cochange", "cochange") + "2016-07-12 16:06:32", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20")), + artifact.type = "Function", + artifact = c("File_Level", "File_Level", "File_Level", "File_Level", "File_Level", "File_Level", + "test3.c::test_function"), + weight = 1, + type = TYPE.EDGES.INTRA, + relation = "cochange" ) if (test.directed) { @@ -216,30 +229,37 @@ patrick::with_parameters_test_that("Network construction with cochange as relati "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526"), + "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:06:10", - "2016-07-12 16:06:32")), + "2016-07-12 16:06:32", + "2016-07-12 16:06:20")), kind = TYPE.COMMIT, type = TYPE.COMMIT ) edges = data.frame( from = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "3a0ed78458b3976243db6829f63eba3eead26774", - "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61"), + "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", + "7d5219c4ba15b8962203f0ae37f9854167914915"), to = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - artifact.type = c("Feature", "Feature", "Feature", "Feature"), - artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature"), - weight = c(1, 1, 1, 1), - type = c(TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA), - relation = c("cochange", "cochange", "cochange", "cochange") + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", + "0a1a5c523d835459c42f33e863623138555e2526"), + date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + artifact.type = "Feature", + artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature", "foo", "foo", "foo"), + weight = 1, + type = TYPE.EDGES.INTRA, + relation = "cochange" ) if (test.directed) { - edges <- edges[, c(2, 1, 3, 4, 5, 6, 7, 8), ] + edges = edges[, c(2, 1, 3, 4, 5, 6, 7, 8), ] } network = igraph::graph_from_data_frame(edges, directed = test.directed, vertices = vertices) network = convert.edge.attributes.to.list(network) @@ -269,31 +289,39 @@ test_that("Adding vertex attributes to a commit network", { "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526"), + "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:06:10", - "2016-07-12 16:06:32")), + "2016-07-12 16:06:32", + "2016-07-12 16:06:20")), kind = TYPE.COMMIT, type = TYPE.COMMIT, author.name = c("Björn", "Olaf", "Olaf", "Karl", + "Thomas", "Thomas") ) edges = data.frame( from = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "3a0ed78458b3976243db6829f63eba3eead26774", - "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61"), + "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", + "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", + "0a1a5c523d835459c42f33e863623138555e2526"), to = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - artifact.type = c("Feature", "Feature", "Feature", "Feature"), - artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature"), - weight = c(1, 1, 1, 1), - type = c(TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA), - relation = c("cochange", "cochange", "cochange", "cochange") + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", + "7d5219c4ba15b8962203f0ae37f9854167914915"), + date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + artifact.type = "Feature", + artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature", "foo", "foo", "foo"), + weight = 1, + type = TYPE.EDGES.INTRA, + relation = "cochange" ) network = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) @@ -309,37 +337,45 @@ test_that("Adding vertex attributes to a commit network", { "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526"), + "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915"), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:06:10", - "2016-07-12 16:06:32")), + "2016-07-12 16:06:32", + "2016-07-12 16:06:20")), kind = TYPE.COMMIT, type = TYPE.COMMIT, author.name = c("Björn", "Olaf", "Olaf", "Karl", + "Thomas", "Thomas"), commit.id = c("", "", - "", "", "") + "", "", "", "") ) edges = data.frame( from = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "3a0ed78458b3976243db6829f63eba3eead26774", - "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61"), + "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", + "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", + "0a1a5c523d835459c42f33e863623138555e2526"), to = c("5a5ec9675e98187e1e92561e1888aa6f04faa338", "1143db502761379c2bfcecc2007fc34282e7ee61", - "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), - date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - artifact.type = c("Feature", "Feature", "Feature", "Feature"), - artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature"), - weight = c(1, 1, 1, 1), - type = c(TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA, TYPE.EDGES.INTRA), - relation = c("cochange", "cochange", "cochange", "cochange") + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", + "7d5219c4ba15b8962203f0ae37f9854167914915"), + date = get.date.from.string(c("2016-07-12 16:00:45", "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + artifact.type = "Feature", + artifact = c("A", "Base_Feature", "Base_Feature", "Base_Feature", "foo", "foo", "foo"), + weight = 1, + type = TYPE.EDGES.INTRA, + relation = "cochange" ) network.two = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) network.two = convert.edge.attributes.to.list(network.two) expect_true(igraph::identical_graphs(network.new.attr, network.two)) -}) \ No newline at end of file +}) diff --git a/tests/test-networks-covariates.R b/tests/test-networks-covariates.R index fea0bba5..8759eb21 100644 --- a/tests/test-networks-covariates.R +++ b/tests/test-networks-covariates.R @@ -131,7 +131,7 @@ get.expected.first.activity = function() { ), list( mails = NA, - commits = "2016-07-12 16:06:32 UTC", + commits = "2016-07-12 16:06:20 UTC", issues = NA ) ) @@ -164,7 +164,7 @@ get.expected.first.activity = function() { ), list( mails = "2016-07-12 16:04:40 UTC", - commits = "2016-07-12 16:06:32 UTC", + commits = "2016-07-12 16:06:20 UTC", issues = NA ) ) @@ -197,7 +197,7 @@ get.expected.first.activity = function() { ), list( mails = "2016-07-12 16:04:40 UTC", - commits = "2016-07-12 16:06:32 UTC", + commits = "2016-07-12 16:06:20 UTC", issues = NA ) ) @@ -230,7 +230,7 @@ get.expected.first.activity = function() { ), list( mails = "2016-07-12 16:04:40 UTC", - commits = "2016-07-12 16:06:32 UTC", + commits = "2016-07-12 16:06:20 UTC", issues = NA ) ) @@ -263,7 +263,7 @@ get.expected.first.activity = function() { ), list( mails = "2016-07-12 16:04:40 UTC", - commits = "2016-07-12 16:06:32 UTC", + commits = "2016-07-12 16:06:20 UTC", issues = NA ) ) @@ -296,7 +296,7 @@ get.expected.first.activity = function() { ), list( mails = "2016-07-12 16:04:40 UTC", - commits = "2016-07-12 16:06:32 UTC", + commits = "2016-07-12 16:06:20 UTC", issues = NA ) ) @@ -670,12 +670,12 @@ test_that("Test add.vertex.attribute.author.commit.count", { networks.and.data = get.network.covariates.test.networks() expected.attributes = list( - range = network.covariates.test.build.expected(c(1L), c(1L), c(1L, 1L, 1L)), - cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 1L)), - all.ranges = network.covariates.test.build.expected(c(1L), c(2L), c(2L, 1L, 1L)), - project.cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 1L)), - project.all.ranges = network.covariates.test.build.expected(c(1L), c(2L), c(2L, 1L, 1L)), - complete = network.covariates.test.build.expected(c(1L), c(2L), c(2L, 1L, 1L)) + range = network.covariates.test.build.expected(c(1L), c(1L), c(1L, 1L, 2L)), + cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 2L)), + all.ranges = network.covariates.test.build.expected(c(1L), c(2L), c(2L, 1L, 2L)), + project.cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 2L)), + project.all.ranges = network.covariates.test.build.expected(c(1L), c(2L), c(2L, 1L, 2L)), + complete = network.covariates.test.build.expected(c(1L), c(2L), c(2L, 1L, 2L)) ) ## Test @@ -698,12 +698,12 @@ test_that("Test add.vertex.attribute.author.commit.count.committer.and.author", networks.and.data = get.network.covariates.test.networks() expected.attributes = list( - range = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 1L)), - cumulative = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 1L)), - all.ranges = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 1L)), - project.cumulative = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 1L)), - project.all.ranges = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 1L)), - complete = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 1L)) + range = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 2L)), + cumulative = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 2L)), + all.ranges = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 2L)), + project.cumulative = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 2L)), + project.all.ranges = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 2L)), + complete = network.covariates.test.build.expected(c(1L), c(0L), c(0L, 1L, 2L)) ) ## Test @@ -726,12 +726,12 @@ test_that("Test add.vertex.attribute.author.commit.count.committer.or.author", { networks.and.data = get.network.covariates.test.networks() expected.attributes = list( - range = network.covariates.test.build.expected(c(1L), c(1L), c(1L, 1L, 2L)), - cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 2L)), - all.ranges = network.covariates.test.build.expected(c(2L), c(2L), c(2L, 1L, 2L)), - project.cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 2L)), - project.all.ranges = network.covariates.test.build.expected(c(2L), c(2L), c(2L, 1L, 2L)), - complete = network.covariates.test.build.expected(c(2L), c(2L), c(2L, 1L, 2L)) + range = network.covariates.test.build.expected(c(1L), c(1L), c(1L, 1L, 3L)), + cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 3L)), + all.ranges = network.covariates.test.build.expected(c(2L), c(2L), c(2L, 1L, 3L)), + project.cumulative = network.covariates.test.build.expected(c(1L), c(1L), c(2L, 1L, 3L)), + project.all.ranges = network.covariates.test.build.expected(c(2L), c(2L), c(2L, 1L, 3L)), + complete = network.covariates.test.build.expected(c(2L), c(2L), c(2L, 1L, 3L)) ) ## Test @@ -1138,7 +1138,7 @@ test_that("Test add.vertex.attribute.author.first.activity with multiple types a list(list(all.activities = "2016-07-12 16:00:45 UTC")), list(list(all.activities = "2016-07-12 16:05:37 UTC"), list(all.activities = "2016-07-12 16:06:10 UTC"), - list(all.activities = "2016-07-12 16:06:32 UTC") + list(all.activities = "2016-07-12 16:06:20 UTC") ) ), cumulative = network.covariates.test.build.expected( @@ -1488,47 +1488,47 @@ test_that("Test add.vertex.attribute.author.role.simple", { c("core"), c("core"), c("core", "core", "core") ), loc.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "core") + c("core"), c("core"), c("peripheral", "core", "core") ) ), cumulative = list( commit.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ), loc.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ) ), all.ranges = list( commit.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ), loc.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ) ), project.cumulative = list( commit.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ), loc.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ) ), project.all.ranges = list( commit.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ), loc.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ) ), complete = list( commit.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ), loc.count = network.covariates.test.build.expected( - c("core"), c("core"), c("core", "core", "peripheral") + c("core"), c("core"), c("core", "peripheral", "core") ) ) ) @@ -1639,27 +1639,27 @@ test_that("Test add.vertex.attribute.artifact.first.occurrence", { expected.attributes = list( range = network.covariates.test.build.expected( c("2016-07-12 15:58:59 UTC"), c("2016-07-12 16:00:45 UTC"), - c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:32 UTC") + c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:20 UTC") ), cumulative = network.covariates.test.build.expected( c("2016-07-12 15:58:59 UTC"), c("2016-07-12 15:58:59 UTC"), - c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:32 UTC") + c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:20 UTC") ), all.ranges = network.covariates.test.build.expected( c("2016-07-12 15:58:59 UTC"), c("2016-07-12 15:58:59 UTC"), - c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:32 UTC") + c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:20 UTC") ), project.cumulative = network.covariates.test.build.expected( c("2016-07-12 15:58:59 UTC"), c("2016-07-12 15:58:59 UTC"), - c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:32 UTC") + c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:20 UTC") ), project.all.ranges = network.covariates.test.build.expected( c("2016-07-12 15:58:59 UTC"), c("2016-07-12 15:58:59 UTC"), - c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:32 UTC") + c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:20 UTC") ), complete = network.covariates.test.build.expected( c("2016-07-12 15:58:59 UTC"), c("2016-07-12 15:58:59 UTC"), - c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:32 UTC") + c("2016-07-12 16:05:41 UTC", "2016-07-12 16:06:20 UTC") ) ) @@ -1746,17 +1746,17 @@ test_that("Test add.vertex.attribute.artifact.change.count", { expected.attributes = list( range = network.covariates.test.build.expected( - c(1L), c(1L), c(3L, 1L)), + c(1L), c(1L), c(3L, 2L)), cumulative = network.covariates.test.build.expected( - c(1L), c(2L), c(3L, 1L)), + c(1L), c(2L), c(3L, 2L)), all.ranges = network.covariates.test.build.expected( - c(2L), c(2L), c(3L, 1L)), + c(2L), c(2L), c(3L, 2L)), project.cumulative = network.covariates.test.build.expected( - c(1L), c(2L), c(3L, 1L)), + c(1L), c(2L), c(3L, 2L)), project.all.ranges = network.covariates.test.build.expected( - c(2L), c(2L), c(3L, 1L)), + c(2L), c(2L), c(3L, 2L)), complete = network.covariates.test.build.expected( - c(2L), c(2L), c(3L, 1L)) + c(2L), c(2L), c(3L, 2L)) ) ## Test @@ -2866,7 +2866,7 @@ test_that("Test get.first.activity.data with missing commits, mails, and issues" "Olaf" = list(commits = get.date.from.string("2016-07-12 16:00:45"), mails = get.date.from.string("2016-07-12 15:58:50"), issues = get.date.from.string("2013-05-25 03:25:06")), - "Thomas" = list(commits = get.date.from.string("2016-07-12 16:06:32"), + "Thomas" = list(commits = get.date.from.string("2016-07-12 16:06:20"), mails = get.date.from.string("2016-07-12 16:04:40"), issues = get.date.from.string("2013-04-21 23:52:09")), "Fritz fritz@example.org" = list(commits = get.date.from.string(NA), diff --git a/tests/test-networks-multi-relation.R b/tests/test-networks-multi-relation.R index 7551a9e7..9bbb53f7 100644 --- a/tests/test-networks-multi-relation.R +++ b/tests/test-networks-multi-relation.R @@ -253,14 +253,14 @@ test_that("Construction of the multi network for the feature artifact with autho ## 2) construct expected edge attributes (data sorted by 'author.name') edges = data.frame(from = c("Björn", "Björn", "Olaf", "Olaf", "Olaf", "Olaf", "Karl", "Karl", # author cochange "Björn", "Björn", "Olaf", "Olaf", # author mail - "Base_Feature", # artifact cochange - "Björn", "Olaf", "Olaf", "Karl", "Thomas", "Thomas", # bipartite cochange + "Base_Feature", "foo", # artifact cochange + "Björn", "Olaf", "Olaf", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", # bipartite cochange "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", # bipartite issue "Olaf", "Olaf", "Olaf", "Olaf", "Olaf", "Olaf", "Karl", "Thomas", "Thomas", "Thomas"), to = c("Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", # author cochange "Olaf", "Olaf", "Thomas", "Thomas", # author mail - "foo", # artifact cochange - "A", "A", "Base_Feature", "Base_Feature", "Base_Feature", "foo", # bipartite cochange + "foo", "foo", # artifact cochange + "A", "A", "Base_Feature", "Base_Feature", "foo", "foo", "Base_Feature", "foo", # bipartite cochange "", "", "", "", # bipartite issue "", "", "", "", "", "", "", "", "", "", "", @@ -270,57 +270,59 @@ test_that("Construction of the multi network for the feature artifact with autho "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 15:58:40", "2016-07-12 15:58:50", "2016-07-12 16:04:40", "2016-07-12 16:05:37", - "2016-07-12 16:06:32", # artifact cochange + "2016-07-12 16:06:32", "2016-07-12 16:06:20", # artifact cochange "2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", # bipartite cochange - "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", - "2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", # bipartite issue + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32", "2016-07-12 16:06:32", "2013-05-05 21:46:30", + "2013-05-05 21:49:21", "2013-05-05 21:49:34", # bipartite issue "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", "2016-07-12 15:59:59", "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59")), - artifact.type = c(rep("Feature", 8), rep("Mail", 4), rep("Feature", 1), rep("Feature", 6), rep("IssueEvent", 21)), + artifact.type = c(rep("Feature", 8), rep("Mail", 4), rep("Feature", 2), rep("Feature", 8), rep("IssueEvent", 21)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", # author cochange "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", "1143db502761379c2bfcecc2007fc34282e7ee61", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 4)), # author mail - "0a1a5c523d835459c42f33e863623138555e2526", # artifact cochange + "0a1a5c523d835459c42f33e863623138555e2526", "7d5219c4ba15b8962203f0ae37f9854167914915", # artifact cochange "72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", # bipartite cochange "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 21)))), # bipartite issue file = I(c("test.c", "test.c", "test2.c", "test3.c", "test2.c", "test2.c", "test3.c", "test2.c", # author cochange as.list(rep(NA, 4)), - "test2.c", # artifact cochange - "test.c", "test.c", "test2.c", "test3.c", "test2.c", "test2.c", # bipartite cochange + "test2.c", "test3.c", # artifact cochange + "test.c", "test.c", "test2.c", "test3.c", "test2.c", "test3.c", "test2.c", "test2.c", # bipartite cochange as.list(rep(NA, 21)))), artifact = I(c("A", "A", "Base_Feature", "Base_Feature", "Base_Feature", "Base_Feature", "Base_Feature", # author cochange "Base_Feature", as.list(rep(NA, 4)), - NA, # artifact cochange - "A", "A", "Base_Feature", "Base_Feature", "Base_Feature", "foo", # bipartite cochange + NA, NA, # artifact cochange + "A", "A", "Base_Feature", "Base_Feature", "foo", "foo", "Base_Feature", "foo", # bipartite cochange as.list(rep(NA, 21)))), weight = 1, - type = c(rep(TYPE.EDGES.INTRA, 13), rep(TYPE.EDGES.INTER, 27)), - relation = c(rep("cochange", 8), rep("mail", 4), rep("cochange", 1), rep("cochange", 6), rep("issue", 21)), + type = c(rep(TYPE.EDGES.INTRA, 14), rep(TYPE.EDGES.INTER, 29)), + relation = c(rep("cochange", 8), rep("mail", 4), rep("cochange", 2), rep("cochange", 8), rep("issue", 21)), message.id = I(c(as.list(rep(NA, 8)), "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", "<65a1sf31sagd684dfv31@mail.gmail.com>", "<9b06e8d20801220234h659c18a3g95c12ac38248c7e0@mail.gmail.com>", - as.list(rep(NA, 28)))), + as.list(rep(NA, 31)))), thread = I(c(as.list(rep(NA, 8)), "", "", "", "", - as.list(rep(NA, 28)))), - author.name = I(c(as.list(rep(NA, 12)), "Thomas", as.list(rep(NA, 27)))), - issue.id = I(c(as.list(rep(NA, 19)), + as.list(rep(NA, 31)))), + author.name = I(c(as.list(rep(NA, 12)), "Thomas", "Thomas", as.list(rep(NA, 29)))), + issue.id = I(c(as.list(rep(NA, 22)), "", "", "", "", # bipartite issue "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "")), - event.name = I(c(as.list(rep(NA, 19)), rep("commented", 21))) + event.name = I(c(as.list(rep(NA, 22)), rep("commented", 21))) ) ## Remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` @@ -379,23 +381,25 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "") edges = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Björn", - "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", - "Björn", "Björn", "Björn", "Karl", "Max", "Max", "Max", - "Olaf", "Olaf", "Olaf", "Olaf", "Olaf", "Olaf", "Thomas", - "Thomas", "Thomas"), - to = c("A", "Base_Feature", "A", - "Base_Feature", "Base_Feature", "foo", + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas", + "Thomas", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", + "Björn", "Björn", "Björn", "Björn", "Björn", "Karl", "Max", + "Max", "Max", "Olaf", "Olaf", "Olaf", "Olaf", "Olaf", + "Olaf", "Thomas", "Thomas", "Thomas"), + to = c("A", "Base_Feature", "A", + "Base_Feature", "foo", "foo", + "Base_Feature", "foo", "", "","","", + "","","", + "","", "", + "", "", "", + "","","", "","","", - "", "","", - "","", "", - "","","", - "","","", - "","", "", - "","", ""), + "", "", "", + "", ""), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", "2016-07-12 16:05:41", + "2016-07-12 16:06:20", "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:32", "2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", "2013-05-06 01:04:34", @@ -409,17 +413,18 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59")), - artifact.type = c(rep("Feature", 6), rep("IssueEvent", 24)), + artifact.type = c(rep("Feature", 8), rep("IssueEvent", 24)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 24)))), - file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c", as.list(rep(NA, 24)))), - artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 24)))), + file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c", as.list(rep(NA, 24)))), + artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo", as.list(rep(NA, 24)))), weight = 1, type = "Bipartite", - relation = c(rep("cochange", 6), rep("issue", 24)), - issue.id = I(c(as.list(rep(NA, 6)), + relation = c(rep("cochange", 8), rep("issue", 24)), + issue.id = I(c(as.list(rep(NA, 8)), "", "", "", "", "", "", "", "", "", @@ -428,7 +433,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "", "", "", "", "")), - event.name = I(c(as.list(rep(NA, 6)), rep("commented", 24))) + event.name = I(c(as.list(rep(NA, 8)), rep("commented", 24))) ) ## Remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` @@ -486,32 +491,33 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "") edges = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Björn", "Björn", - "Björn", "Fritz fritz@example.org", "georg", "Hans", "Hans", "Hans", + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas", "Thomas", "Björn", + "Björn", "Björn", "Fritz fritz@example.org", "georg", "Hans", "Hans", "Hans", "Hans", "Hans", "Hans", "Hans", "Olaf", "Olaf", "Thomas", "udo"), - to = c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", "", + to = c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32", - "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", - "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", - "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", - "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", - "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", - "2010-07-12 10:05:36")), - artifact.type = c(rep("Feature", 6), rep("Mail", 16)), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32", "2016-07-12 16:06:32", "2004-10-09 18:38:13", + "2005-02-09 18:49:49", "2016-07-12 15:58:40", "2010-07-12 11:05:35", + "2010-07-12 12:05:34", "2010-07-12 12:05:40", "2010-07-12 12:05:41", + "2010-07-12 12:05:42", "2010-07-12 12:05:43", "2010-07-12 12:05:44", + "2010-07-12 12:05:45", "2010-07-12 12:05:46", "2016-07-12 15:58:50", + "2016-07-12 16:05:37", "2016-07-12 16:04:40", "2010-07-12 10:05:36")), + artifact.type = c(rep("Feature", 8), rep("Mail", 16)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 16)))), - file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c", as.list(rep(NA, 16)))), - artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 16)))), + file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c", as.list(rep(NA, 16)))), + artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo", as.list(rep(NA, 16)))), weight = 1, type = "Bipartite", - relation = c(rep("cochange", 6), rep("mail", 16)), - message.id = I(c(as.list(rep(NA, 6)), "", + relation = c(rep("cochange", 8), rep("mail", 16)), + message.id = I(c(as.list(rep(NA, 8)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "", "", "", "", "", @@ -520,7 +526,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "<9b06e8d20801220234h659c18a3g95c12ac38248c7e0@mail.gmail.com>", "<65a1sf31sagd684dfv31@mail.gmail.com>", "" )), - thread = I(c(as.list(rep(NA, 6)), "", "", "", "", + thread = I(c(as.list(rep(NA, 8)), "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "")) @@ -696,13 +702,13 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "") edges = data.frame( - from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Björn", "Björn", - "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", + from = c("Björn", "Karl", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas", "Thomas", "Björn", + "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Björn", "Karl", "Max", "Max", "Max", "Olaf", "Olaf", "Olaf", "Olaf", "Olaf", "Olaf", "Thomas", "Thomas", "Thomas", "Björn", "Björn", "Björn", "Fritz fritz@example.org", "georg", "Hans", "Hans", "Hans", "Hans", "Hans", "Hans", "Hans", "Olaf", "Olaf", "Thomas", "udo"), - to = c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", "", + to = c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo", "", "", "", "", "", "", "", "", "", "", @@ -715,32 +721,33 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "", "", "", "", ""), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:00:45", - "2016-07-12 16:05:41", "2016-07-12 16:06:32", "2016-07-12 16:06:32", - "2013-05-05 21:46:30", "2013-05-05 21:49:21", "2013-05-05 21:49:34", - "2013-05-06 01:04:34", "2013-05-25 03:48:41", "2013-05-25 04:08:07", - "2016-07-12 14:59:25", "2016-07-12 16:02:30", "2016-07-12 16:06:01", - "2016-07-15 19:55:39", "2017-05-23 12:32:39", "2016-07-12 15:59:59", - "2016-07-15 20:07:47", "2016-07-27 20:12:08", "2016-07-28 06:27:52", - "2013-05-25 03:25:06", "2013-05-25 06:06:53", "2013-05-25 06:22:23", - "2013-06-01 06:50:26", "2016-07-12 16:01:01", "2016-07-12 16:02:02", - "2013-04-21 23:52:09", "2016-07-12 15:59:25", "2016-07-12 16:03:59", - "2004-10-09 18:38:13", "2005-02-09 18:49:49", "2016-07-12 15:58:40", - "2010-07-12 11:05:35", "2010-07-12 12:05:34", "2010-07-12 12:05:40", - "2010-07-12 12:05:41", "2010-07-12 12:05:42", "2010-07-12 12:05:43", - "2010-07-12 12:05:44", "2010-07-12 12:05:45", "2010-07-12 12:05:46", - "2016-07-12 15:58:50", "2016-07-12 16:05:37", "2016-07-12 16:04:40", - "2010-07-12 10:05:36")), - artifact.type = c(rep("Feature", 6), rep("IssueEvent", 24), rep("Mail", 16)), + "2016-07-12 16:05:41", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:32", "2016-07-12 16:06:32", "2013-05-05 21:46:30", + "2013-05-05 21:49:21", "2013-05-05 21:49:34", "2013-05-06 01:04:34", + "2013-05-25 03:48:41", "2013-05-25 04:08:07", "2016-07-12 14:59:25", + "2016-07-12 16:02:30", "2016-07-12 16:06:01", "2016-07-15 19:55:39", + "2017-05-23 12:32:39", "2016-07-12 15:59:59", "2016-07-15 20:07:47", + "2016-07-27 20:12:08", "2016-07-28 06:27:52", "2013-05-25 03:25:06", + "2013-05-25 06:06:53", "2013-05-25 06:22:23", "2013-06-01 06:50:26", + "2016-07-12 16:01:01", "2016-07-12 16:02:02", "2013-04-21 23:52:09", + "2016-07-12 15:59:25", "2016-07-12 16:03:59", "2004-10-09 18:38:13", + "2005-02-09 18:49:49", "2016-07-12 15:58:40", "2010-07-12 11:05:35", + "2010-07-12 12:05:34", "2010-07-12 12:05:40", "2010-07-12 12:05:41", + "2010-07-12 12:05:42", "2010-07-12 12:05:43", "2010-07-12 12:05:44", + "2010-07-12 12:05:45", "2010-07-12 12:05:46", "2016-07-12 15:58:50", + "2016-07-12 16:05:37", "2016-07-12 16:04:40", "2010-07-12 10:05:36")), + artifact.type = c(rep("Feature", 8), rep("IssueEvent", 24), rep("Mail", 16)), hash = I(c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "1143db502761379c2bfcecc2007fc34282e7ee61", "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526", as.list(rep(NA, 40)))), - file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test2.c", as.list(rep(NA, 40)))), - artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "Base_Feature", "foo", as.list(rep(NA, 40)))), + file = I(c("test.c", "test3.c", "test.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c", as.list(rep(NA, 40)))), + artifact = I(c("A", "Base_Feature", "A", "Base_Feature", "foo", "foo", "Base_Feature", "foo", as.list(rep(NA, 40)))), weight = 1, type = "Bipartite", - relation = c(rep("cochange", 6), rep("issue", 24), rep("mail", 16)), - issue.id = I(c(as.list(rep(NA, 6)), "", "", + relation = c(rep("cochange", 8), rep("issue", 24), rep("mail", 16)), + issue.id = I(c(as.list(rep(NA, 8)), "", "", "", "", "", "", "", "", "", "", "", @@ -749,8 +756,8 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "", "", "", "", "", "", as.list(rep(NA, 16)))), - event.name = I(c(as.list(rep(NA, 6)), rep("commented", 24), as.list(rep(NA, 16)))), - message.id = I(c(as.list(rep(NA, 30)), "", + event.name = I(c(as.list(rep(NA, 8)), rep("commented", 24), as.list(rep(NA, 16)))), + message.id = I(c(as.list(rep(NA, 32)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "", "", "", "", "", @@ -758,7 +765,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re "", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", "<9b06e8d20801220234h659c18a3g95c12ac38248c7e0@mail.gmail.com>", "<65a1sf31sagd684dfv31@mail.gmail.com>", "")), - thread = I(c(as.list(rep(NA, 30)), "", "", "", "", + thread = I(c(as.list(rep(NA, 32)), "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "")) diff --git a/tests/test-networks-multi.R b/tests/test-networks-multi.R index 52770fa4..cbdd19da 100644 --- a/tests/test-networks-multi.R +++ b/tests/test-networks-multi.R @@ -37,67 +37,65 @@ if (!dir.exists(CF.DATA)) CF.DATA = file.path(".", "tests", "codeface-data") test_that("Construction of the multi network for the feature artifact with author.relation = 'cochange' and artifact. relation = 'cochange'.", { - ## configurations - proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) - proj.conf$update.value("commits.filter.base.artifact", FALSE) - net.conf = NetworkConf$new() - net.conf$update.values(updated.values = list(author.relation = "cochange", artifact.relation = "cochange")) + ## configurations + proj.conf = ProjectConf$new(CF.DATA, CF.SELECTION.PROCESS, CASESTUDY, ARTIFACT) + proj.conf$update.value("commits.filter.base.artifact", FALSE) + net.conf = NetworkConf$new() + net.conf$update.values(updated.values = list(author.relation = "cochange", artifact.relation = "cochange")) - ## construct objects - proj.data = ProjectData$new(project.conf = proj.conf) - network.builder = NetworkBuilder$new(project.data = proj.data, network.conf = net.conf) + ## construct objects + proj.data = ProjectData$new(project.conf = proj.conf) + network.builder = NetworkBuilder$new(project.data = proj.data, network.conf = net.conf) - ## build network - network.built = network.builder$get.multi.network() + ## build network + network.built = network.builder$get.multi.network() - ## build expected network - vertices = data.frame(name = c("Björn", "Olaf", "Karl", "Thomas", - "Base_Feature", "foo", "A"), - kind = c(rep(TYPE.AUTHOR, 4), rep("Feature", 3)), - type = c(rep(TYPE.AUTHOR, 4), rep(TYPE.ARTIFACT, 3)) - ) - row.names(vertices) = c("Björn", "Olaf", "Karl", "Thomas", - "Base_Feature", "foo", "A") - edges = data.frame( - from = c("Björn", "Björn", "Olaf", "Olaf", "Olaf", "Olaf", "Karl", "Karl", - "Base_Feature", "Björn", "Olaf", "Olaf", "Karl", "Thomas", - "Thomas"), - to = c("Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", - "foo", "A", "A", "Base_Feature", "Base_Feature", "Base_Feature", "foo"), - date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", - "2016-07-12 16:06:10", "2016-07-12 16:05:41", "2016-07-12 16:06:32", - "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", - "2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", - "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), - artifact.type = c("Feature", "Feature", "Feature", "Feature", "Feature", "Feature", "Feature", - "Feature", "Feature", "Feature", "Feature", "Feature", "Feature", "Feature", - "Feature"), - hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", - "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", - "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", - "1143db502761379c2bfcecc2007fc34282e7ee61", "0a1a5c523d835459c42f33e863623138555e2526", - "0a1a5c523d835459c42f33e863623138555e2526", "72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", - "5a5ec9675e98187e1e92561e1888aa6f04faa338", "3a0ed78458b3976243db6829f63eba3eead26774", - "1143db502761379c2bfcecc2007fc34282e7ee61", "0a1a5c523d835459c42f33e863623138555e2526", - "0a1a5c523d835459c42f33e863623138555e2526"), - file = c("test.c", "test.c", "test2.c", "test3.c", "test2.c", "test2.c", "test3.c", "test2.c", - "test2.c", "test.c", "test.c", "test2.c", "test3.c", "test2.c", "test2.c"), - artifact = I(list("A", "A", "Base_Feature", "Base_Feature", "Base_Feature", "Base_Feature", "Base_Feature", - "Base_Feature", NA, "A", "A", "Base_Feature", "Base_Feature", "Base_Feature", - "foo")), - weight = 1, - type = c(rep(TYPE.EDGES.INTRA, 9), rep(TYPE.EDGES.INTER, 6)), - relation = "cochange", - author.name = I(list(NA, NA, NA, NA, NA, NA, NA, NA, "Thomas", NA, NA, NA, NA, NA, NA)) - ) + ## build expected network + vertices = data.frame(name = c("Björn", "Olaf", "Karl", "Thomas", + "Base_Feature", "foo", "A"), + kind = c(rep(TYPE.AUTHOR, 4), rep("Feature", 3)), + type = c(rep(TYPE.AUTHOR, 4), rep(TYPE.ARTIFACT, 3))) + row.names(vertices) = c("Björn", "Olaf", "Karl", "Thomas", + "Base_Feature", "foo", "A") + edges = data.frame(from = c("Björn", "Björn", "Olaf", "Olaf", "Olaf", "Olaf", "Karl", "Karl", "Base_Feature", + "foo", "Björn", "Olaf", "Olaf", "Karl", "Thomas", "Thomas", "Thomas", "Thomas"), + to = c("Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas", + "foo", "foo", "A", "A", "Base_Feature", "Base_Feature", "foo", "foo", "Base_Feature", "foo"), + date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", + "2016-07-12 16:06:10", "2016-07-12 16:05:41", "2016-07-12 16:06:32", + "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20", "2016-07-12 15:58:59", "2016-07-12 16:00:45", + "2016-07-12 16:05:41", "2016-07-12 16:06:10", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + artifact.type = c("Feature", "Feature", "Feature", "Feature", "Feature", "Feature", "Feature", + "Feature", "Feature", "Feature", "Feature", "Feature", "Feature", "Feature", + "Feature", "Feature", "Feature", "Feature"), + hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", + "3a0ed78458b3976243db6829f63eba3eead26774", "0a1a5c523d835459c42f33e863623138555e2526", + "1143db502761379c2bfcecc2007fc34282e7ee61", "0a1a5c523d835459c42f33e863623138555e2526", + "0a1a5c523d835459c42f33e863623138555e2526", "7d5219c4ba15b8962203f0ae37f9854167914915", + "72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", + "0a1a5c523d835459c42f33e863623138555e2526", "0a1a5c523d835459c42f33e863623138555e2526"), + file = c("test.c", "test.c", "test2.c", "test3.c", "test2.c", "test2.c", "test3.c", "test2.c", "test2.c", + "test3.c", "test.c", "test.c", "test2.c", "test3.c", "test2.c", "test3.c", "test2.c", "test2.c"), + artifact = I(list("A", "A", "Base_Feature", "Base_Feature", "Base_Feature", "Base_Feature", "Base_Feature", + "Base_Feature", NA, NA, "A", "A", "Base_Feature", "Base_Feature", "foo", "foo", + "Base_Feature", "foo")), + weight = 1, + type = c(rep(TYPE.EDGES.INTRA, 10), rep(TYPE.EDGES.INTER, 8)), + relation = "cochange", + author.name = I(list(NA, NA, NA, NA, NA, NA, NA, NA, "Thomas", "Thomas", NA, NA, NA, NA, NA, NA, NA, NA))) - ## remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` - edges[["artifact"]] = unclass(edges[["artifact"]]) - edges[["author.name"]] = unclass(edges[["author.name"]]) + ## remove the 'AsIs' class from the edge attributes that have been inserted via `I(...)` + edges[["artifact"]] = unclass(edges[["artifact"]]) + edges[["author.name"]] = unclass(edges[["author.name"]]) - network.expected = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) - network.expected = convert.edge.attributes.to.list(network.expected) + network.expected = igraph::graph_from_data_frame(edges, directed = FALSE, vertices = vertices) + network.expected = convert.edge.attributes.to.list(network.expected) - assert.networks.equal(network.expected, network.built) - }) + assert.networks.equal(network.expected, network.built) +}) diff --git a/tests/test-read.R b/tests/test-read.R index bf89594f..a432bbff 100644 --- a/tests/test-read.R +++ b/tests/test-read.R @@ -49,44 +49,49 @@ test_that("Read the raw commit data with the feature artifact.", { commit.data.read = read.commits(proj.conf$get.value("datapath"), proj.conf$get.value("artifact")) ## build the expected data.frame - commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32712, 32713, 32713, 32710, 32710, 32714, 32707, 32709, - 32711, 32711)), + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32712, 32713, 32713, 32710, 32710, 32714, 32707, 32708, + 32708, 32709, 32711, 32711)), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 15:58:59", "2016-07-12 16:00:45", - "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:05:41", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + "2016-07-12 16:00:45", "2016-07-12 16:05:41", "2016-07-12 16:05:41", + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", + "2016-07-12 16:06:32")), author.name = c("Björn", "Björn", "Olaf", "Olaf", "Olaf", "Olaf", "Karl", "Karl", "Thomas", - "Thomas", "Thomas"), + "Thomas", "Thomas", "Thomas", "Thomas"), author.email = c("bjoern@example.org", "bjoern@example.org", "olaf@example.org", - "olaf@example.org", "olaf@example.org", "olaf@example.org", "karl@example.org", - "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org"), + "olaf@example.org", "olaf@example.org", "olaf@example.org", "karl@example.org", + "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org", "thomas@example.org"), committer.date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 15:58:59", "2016-07-20 10:00:44", "2016-07-20 10:00:44", "2016-07-12 17:05:55", "2016-07-12 17:05:55", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32", "2016-07-12 16:06:32")), + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", + "2016-07-12 16:06:32")), committer.name = c("Björn", "Björn", "Björn", "Björn", "Thomas", "Thomas", "Karl", "Karl", "Thomas", - "Thomas", "Thomas"), + "Thomas", "Thomas", "Thomas", "Thomas"), committer.email = c("bjoern@example.org", "bjoern@example.org", "bjoern@example.org", "bjoern@example.org", "thomas@example.org", "thomas@example.org", "karl@example.org", "karl@example.org", - "thomas@example.org", "thomas@example.org", "thomas@example.org"), + "thomas@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org"), hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", - "5a5ec9675e98187e1e92561e1888aa6f04faa338", "5a5ec9675e98187e1e92561e1888aa6f04faa338", - "3a0ed78458b3976243db6829f63eba3eead26774", "3a0ed78458b3976243db6829f63eba3eead26774", - "1143db502761379c2bfcecc2007fc34282e7ee61", "418d1dc4929ad1df251d2aeb833dd45757b04a6f", - "d01921773fae4bed8186b0aa411d6a2f7a6626e6", "0a1a5c523d835459c42f33e863623138555e2526", - "0a1a5c523d835459c42f33e863623138555e2526"), - changed.files = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)), - added.lines = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)), - deleted.lines = as.integer(c(1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)), - diff.size = as.integer(c(2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1)), + "5a5ec9675e98187e1e92561e1888aa6f04faa338", "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "3a0ed78458b3976243db6829f63eba3eead26774", "3a0ed78458b3976243db6829f63eba3eead26774", + "1143db502761379c2bfcecc2007fc34282e7ee61", "418d1dc4929ad1df251d2aeb833dd45757b04a6f", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", + "d01921773fae4bed8186b0aa411d6a2f7a6626e6", "0a1a5c523d835459c42f33e863623138555e2526", + "0a1a5c523d835459c42f33e863623138555e2526"), + changed.files = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1)), + added.lines = as.integer(c(1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 1)), + deleted.lines = as.integer(c(1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0)), + diff.size = as.integer(c(2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1)), file = c("test.c", "test.c", "test.c", "test.c", "test2.c", "test2.c", "test3.c", UNTRACKED.FILE, - UNTRACKED.FILE, "test2.c", "test2.c"), + "test2.c", "test3.c", UNTRACKED.FILE, "test2.c", "test2.c"), artifact = c("A", "defined(A)", "A", "defined(A)", "Base_Feature", "Base_Feature", "Base_Feature", - UNTRACKED.FILE.EMPTY.ARTIFACT, UNTRACKED.FILE.EMPTY.ARTIFACT, "Base_Feature", "foo"), + UNTRACKED.FILE.EMPTY.ARTIFACT, "foo", "foo", UNTRACKED.FILE.EMPTY.ARTIFACT, "Base_Feature", "foo"), artifact.type = c("Feature", "FeatureExpression", "Feature", "FeatureExpression", "Feature", - "FeatureExpression", "Feature", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, - UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "Feature", "Feature"), - artifact.diff.size = as.integer(c(1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1))) + "FeatureExpression", "Feature", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "Feature", + "Feature", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "Feature", "Feature"), + artifact.diff.size = as.integer(c(1, 1, 1, 1, 1, 1, 1, 0, 1, 2, 0, 1, 1))) ## check the results expect_identical(commit.data.read, commit.data.expected, info = "Raw commit data.") @@ -106,32 +111,35 @@ test_that("Read the raw commit data with the file artifact.", { commit.data.read = read.commits(proj.conf$get.value("datapath"), proj.conf$get.value("artifact")) ## build the expected data.frame - commit.data.expected = data.frame(commit.id = format.commit.ids(c(32716, 32717, 32718, 32719, 32720, 32721, 32715)), + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32716, 32717, 32718, 32719, 32720, 32722, 32722, 32721, 32715)), date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:05:41", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32")), - author.name = c("Björn", "Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas"), - author.email = c("bjoern@example.org", "olaf@example.org", "olaf@example.org", - "karl@example.org", "karl@example.org", "thomas@example.org", "thomas@example.org"), + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32")), + author.name = c("Björn", "Olaf", "Olaf", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas"), + author.email = c("bjoern@example.org", "olaf@example.org", "olaf@example.org", "karl@example.org", + "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org"), committer.date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-20 10:00:44", "2016-07-12 17:05:55", - "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:30", - "2016-07-12 16:06:32")), - committer.name = c("Björn", "Björn", "Thomas", "Karl", "Karl", "Thomas", "Thomas"), - committer.email = c("bjoern@example.org", "bjoern@example.org", "thomas@example.org", - "karl@example.org", "karl@example.org", "thomas@example.org", "thomas@example.org"), + "2016-07-12 16:06:10", "2016-07-12 16:06:20", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32")), + committer.name = c("Björn", "Björn", "Thomas", "Karl", "Karl", "Thomas", "Thomas", "Thomas", "Thomas"), + committer.email = c("bjoern@example.org", "bjoern@example.org", "thomas@example.org", "karl@example.org", + "karl@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org", + "thomas@example.org"), hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", - "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", - "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", - "0a1a5c523d835459c42f33e863623138555e2526"), - changed.files = as.integer(c(1, 1, 1, 1, 1, 1, 1)), - added.lines = as.integer(c(1, 1, 1, 1, 1, 1, 1)), - deleted.lines = as.integer(c(1, 0, 0, 0, 0, 0, 0)), - diff.size = as.integer(c(2, 1, 1, 1, 1, 1, 1)), - file = c("test.c", "test.c", "test2.c", "test3.c", UNTRACKED.FILE, UNTRACKED.FILE, "test2.c"), - artifact = c("test.c", "test.c", "test2.c", "test3.c", UNTRACKED.FILE, UNTRACKED.FILE, "test2.c"), - artifact.type = c("File", "File", "File", "File", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, + "3a0ed78458b3976243db6829f63eba3eead26774", "1143db502761379c2bfcecc2007fc34282e7ee61", + "418d1dc4929ad1df251d2aeb833dd45757b04a6f", "7d5219c4ba15b8962203f0ae37f9854167914915", + "7d5219c4ba15b8962203f0ae37f9854167914915", "d01921773fae4bed8186b0aa411d6a2f7a6626e6", + "0a1a5c523d835459c42f33e863623138555e2526"), + changed.files = as.integer(c(1, 1, 1, 1, 1, 2, 2, 1, 1)), + added.lines = as.integer(c(1, 1, 1, 1, 1, 3, 3, 1, 1)), + deleted.lines = as.integer(c(1, 0, 0, 0, 0, 1, 1, 0, 0)), + diff.size = as.integer(c(2, 1, 1, 1, 1, 2, 2, 1, 1)), + file = c("test.c", "test.c", "test2.c", "test3.c", UNTRACKED.FILE, "test2.c", "test3.c", UNTRACKED.FILE, "test2.c"), + artifact = c("test.c", "test.c", "test2.c", "test3.c", UNTRACKED.FILE, "test2.c", "test3.c", UNTRACKED.FILE, "test2.c"), + artifact.type = c("File", "File", "File", "File", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "File", "File", UNTRACKED.FILE.EMPTY.ARTIFACT.TYPE, "File"), - artifact.diff.size = as.integer(c(1, 1, 1, 1, 0, 0, 1))) + artifact.diff.size = as.integer(c(1, 1, 1, 1, 0, 1, 2, 0, 1))) ## check the results expect_identical(commit.data.read, commit.data.expected, info = "Raw commit data.") diff --git a/tests/test-split-data-activity-based.R b/tests/test-split-data-activity-based.R index c2043a6e..f99984a9 100644 --- a/tests/test-split-data-activity-based.R +++ b/tests/test-split-data-activity-based.R @@ -75,8 +75,8 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity ## check time ranges expected = c( "2016-07-12 15:58:59-2016-07-12 16:06:10", - "2016-07-12 16:06:10-2016-07-12 16:06:32", - "2016-07-12 16:06:32-2016-07-12 16:06:33" + "2016-07-12 16:06:10-2016-07-12 16:06:30", + "2016-07-12 16:06:30-2016-07-12 16:06:33" ) lapply(results, function(res) { expect_equal(res$get.project.conf()$get.value("ranges"), expected, @@ -88,7 +88,7 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity info = "Splitting must not modify the original ProjectConf.") ## test that the config contains the correct splitting information - revisions = c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:06:32", "2016-07-12 16:06:33") + revisions = c("2016-07-12 15:58:59", "2016-07-12 16:06:10", "2016-07-12 16:06:30", "2016-07-12 16:06:33") expected.config = list( split.type = "activity-based", split.length = 3, @@ -108,33 +108,33 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity expected.data = list( commits = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$commits[1:3, ], - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$commits[4:6, ], - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$commits[7:8, ] + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$commits[4:7, ], + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$commits[8:10, ] ), commit.messages = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$commit.messages, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$commit.messages, - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$commit.messages + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$commit.messages, + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$commit.messages ), issues = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$issues[rownames(data$issues) %in% c(1:3, 12, 21:25, 29:30, 32:34, 54:55), ], - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$issues[rownames(data$issues) == 4, ], - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$issues[0, ] + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$issues[0, ], + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$issues[rownames(data$issues) == 4, ] ), mails = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$mails[15:16, ], # when pasta is not configured: rownames(data$mails) %in% 16:17 - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$mails[0, ], - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$mails[0, ] + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$mails[0, ], + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$mails[0, ] ), pasta = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$pasta, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$pasta, - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$pasta + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$pasta, + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$pasta ), synchronicity = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$synchronicity, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$synchronicity, - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$synchronicity + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$synchronicity, + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$synchronicity ) ) results.data = list( @@ -176,7 +176,7 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity revisions = c("2016-07-12 15:58:59", "2016-07-12 16:06:33") expected.config = list( split.type = "activity-based", - split.length = 18, + split.length = 20, split.basis = "commits", split.sliding.window = FALSE, split.revisions = revisions, @@ -502,8 +502,8 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity commits = list( "2013-04-21 23:52:09-2013-05-25 06:22:23" = data$commits[0, ], "2013-05-25 06:22:23-2016-07-12 15:59:59" = data$commits[1, ], - "2016-07-12 15:59:59-2016-07-12 16:06:30" = data$commits[2:5, ], - "2016-07-12 16:06:30-2016-08-07 15:37:02" = data$commits[6:8, ], + "2016-07-12 15:59:59-2016-07-12 16:06:30" = data$commits[2:7, ], + "2016-07-12 16:06:30-2016-08-07 15:37:02" = data$commits[8:10, ], "2016-08-07 15:37:02-2017-05-23 12:31:34" = data$commits[0, ], "2017-05-23 12:31:34-2017-05-23 12:32:40" = data$commits[0, ] ), @@ -673,8 +673,9 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity expected = c( "2016-07-12 15:58:59-2016-07-12 16:06:10", "2016-07-12 16:00:45-2016-07-12 16:06:20", - "2016-07-12 16:06:10-2016-07-12 16:06:32", - "2016-07-12 16:06:20-2016-07-12 16:06:33" + "2016-07-12 16:06:10-2016-07-12 16:06:30", + "2016-07-12 16:06:20-2016-07-12 16:06:32", + "2016-07-12 16:06:30-2016-07-12 16:06:33" ) lapply(results, function(res) { expect_equal(res$get.project.conf()$get.value("ranges"), expected, @@ -687,7 +688,8 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity ## test that the config contains the correct splitting information revisions = c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:06:10", - "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:33") + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", + "2016-07-12 16:06:33") expected.config = list( split.type = "activity-based", split.length = 3, @@ -707,39 +709,45 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity commits = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$commits[1:3, ], "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$commits[2:4, ], - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$commits[4:6, ], - "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$commits[5:8, ] + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$commits[4:7, ], + "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$commits[5:8, ], + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$commits[8:10, ] ), commit.messages = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$commit.messages, "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$commit.messages, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$commit.messages, - "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$commit.messages + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$commit.messages, + "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$commit.messages, + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$commit.messages ), issues = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$issues[rownames(data$issues) %in% c(1:3, 12, 21:25, 29:30, 32:34, 54:55), ], "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$issues[rownames(data$issues) %in% c(12, 24:25, 29:30, 32:34, 54:55), ], - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$issues[rownames(data$issues) == 4, ], - "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$issues[rownames(data$issues) == 4, ] + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$issues[0, ], + "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$issues[rownames(data$issues) == 4, ], + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$issues[rownames(data$issues) == 4, ] ), mails = list( ## comments indicate row names when pasta is not configured "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$mails[15:16, ], # rownames(data$mails) %in% 16:17 "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$mails[15:16, ], # rownames(data$mails) %in% 16:17 - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$mails[0, ], - "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$mails[0, ] + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$mails[0, ], + "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$mails[0, ], + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$mails[0, ] ), pasta = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$pasta, "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$pasta, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$pasta, - "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$pasta + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$pasta, + "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$pasta, + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$pasta ), synchronicity = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$synchronicity, "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$synchronicity, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$synchronicity, - "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$synchronicity + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$synchronicity, + "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$synchronicity, + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$synchronicity ) ) results.data = list( @@ -781,7 +789,7 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity revisions = c("2016-07-12 15:58:59", "2016-07-12 16:06:33") expected.config = list( split.type = "activity-based", - split.length = 18, + split.length = 20, split.basis = "commits", split.sliding.window = FALSE, # The sliding-window approach does not apply if we only have one range or less split.revisions = revisions, @@ -869,9 +877,9 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity expected = c( "2016-07-12 15:58:59-2016-07-12 16:06:10", "2016-07-12 16:00:45-2016-07-12 16:06:20", - "2016-07-12 16:06:10-2016-07-12 16:06:32", + "2016-07-12 16:06:10-2016-07-12 16:06:30", "2016-07-12 16:06:20-2016-07-12 16:06:32", - "2016-07-12 16:06:32-2016-07-12 16:06:33" + "2016-07-12 16:06:30-2016-07-12 16:06:33" ) lapply(results, function(res) { expect_equal(res$get.project.conf()$get.value("ranges"), expected, @@ -884,7 +892,7 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity ## test that the config contains the correct splitting information revisions = c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:06:10", - "2016-07-12 16:06:20", "2016-07-12 16:06:32", "2016-07-12 16:06:32", + "2016-07-12 16:06:20", "2016-07-12 16:06:30", "2016-07-12 16:06:32", "2016-07-12 16:06:33") expected.config = list( split.type = "activity-based", @@ -905,45 +913,45 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity commits = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$commits[1:3, ], "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$commits[2:4, ], - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$commits[4:6, ], + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$commits[4:7, ], "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$commits[5:8, ], - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$commits[7:9, ] + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$commits[8:11, ] ), commit.messages = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$commit.messages, "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$commit.messages, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$commit.messages, + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$commit.messages, "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$commit.messages, - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$commit.messages + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$commit.messages ), issues = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$issues[rownames(data$issues) %in% c(1:3, 12, 21:25, 29:30, 32:34, 54:55), ], "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$issues[rownames(data$issues) %in% c(12, 24:25, 29:30, 32:34, 54:55), ], - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$issues[rownames(data$issues) == 4, ], + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$issues[0, ], "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$issues[rownames(data$issues) == 4, ], - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$issues[0, ] + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$issues[rownames(data$issues) == 4, ] ), mails = list( ## comments indicate row names when pasta is not configured "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$mails[15:16, ], # rownames(data$mails) %in% 16:17 "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$mails[15:16, ], # rownames(data$mails) %in% 16:17 - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$mails[0, ], + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$mails[0, ], "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$mails[0, ], - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$mails[0, ] + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$mails[0, ] ), pasta = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$pasta, "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$pasta, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$pasta, + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$pasta, "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$pasta, - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$pasta + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$pasta ), synchronicity = list( "2016-07-12 15:58:59-2016-07-12 16:06:10" = data$synchronicity, "2016-07-12 16:00:45-2016-07-12 16:06:20" = data$synchronicity, - "2016-07-12 16:06:10-2016-07-12 16:06:32" = data$synchronicity, + "2016-07-12 16:06:10-2016-07-12 16:06:30" = data$synchronicity, "2016-07-12 16:06:20-2016-07-12 16:06:32" = data$synchronicity, - "2016-07-12 16:06:32-2016-07-12 16:06:33" = data$synchronicity + "2016-07-12 16:06:30-2016-07-12 16:06:33" = data$synchronicity ) ) results.data = list( @@ -1276,9 +1284,9 @@ patrick::with_parameters_test_that("Split a data object activity-based (activity "2013-05-06 01:04:34-2016-07-12 15:30:02" = data$commits[0, ], "2013-05-25 06:22:23-2016-07-12 15:59:59" = data$commits[1, ], "2016-07-12 15:30:02-2016-07-12 16:02:02" = data$commits[1:2, ], - "2016-07-12 15:59:59-2016-07-12 16:06:30" = data$commits[2:5, ], - "2016-07-12 16:02:02-2016-07-27 20:12:08" = data$commits[3:8, ], - "2016-07-12 16:06:30-2016-08-07 15:37:02" = data$commits[6:8, ], + "2016-07-12 15:59:59-2016-07-12 16:06:30" = data$commits[2:7, ], + "2016-07-12 16:02:02-2016-07-27 20:12:08" = data$commits[3:10, ], + "2016-07-12 16:06:30-2016-08-07 15:37:02" = data$commits[8:10, ], "2016-07-27 20:12:08-2016-10-05 16:45:09" = data$commits[0, ], "2016-08-07 15:37:02-2017-05-23 12:31:34" = data$commits[0, ], "2016-10-05 16:45:09-2017-05-23 12:32:40" = data$commits[0, ] @@ -1500,7 +1508,7 @@ patrick::with_parameters_test_that("Split a data object activity-based (number.w expected.data = list( commits = list( "2016-07-12 15:58:59-2016-07-12 16:06:20" = data$commits[1:4, ], - "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$commits[5:8, ] + "2016-07-12 16:06:20-2016-07-12 16:06:33" = data$commits[5:10, ] ), commit.messages = list( "2016-07-12 15:58:59-2016-07-12 16:06:20" = data$commit.messages, @@ -1757,7 +1765,7 @@ patrick::with_parameters_test_that("Split a data object activity-based (number.w expected.data = list( commits = list( "2013-04-21 23:52:09-2016-07-12 16:03:59" = data$commits[1:2, ], - "2016-07-12 16:03:59-2017-05-23 12:32:40" = data$commits[3:8, ] + "2016-07-12 16:03:59-2017-05-23 12:32:40" = data$commits[3:10, ] ), commit.messages = list( "2013-04-21 23:52:09-2016-07-12 16:03:59" = data$commit.messages, diff --git a/tests/test-split-data-time-based.R b/tests/test-split-data-time-based.R index 40381c6c..86f4d7df 100644 --- a/tests/test-split-data-time-based.R +++ b/tests/test-split-data-time-based.R @@ -108,7 +108,7 @@ patrick::with_parameters_test_that("Split a data object time-based (split.basis commits = list( "2016-07-12 15:58:59-2016-07-12 16:01:59" = data$commits[1:2, ], "2016-07-12 16:01:59-2016-07-12 16:04:59" = data$commits[0, ], - "2016-07-12 16:04:59-2016-07-12 16:06:33" = data$commits[3:8, ] + "2016-07-12 16:04:59-2016-07-12 16:06:33" = data$commits[3:10, ] ), commit.messages = list( "2016-07-12 15:58:59-2016-07-12 16:01:59" = data$commit.messages, @@ -455,8 +455,8 @@ patrick::with_parameters_test_that("Split a data object time-based (split.basis "2016-07-12 15:58:59-2016-07-12 16:01:59" = data$commits[1:2, ], "2016-07-12 16:00:29-2016-07-12 16:03:29" = data$commits[2, ], "2016-07-12 16:01:59-2016-07-12 16:04:59" = data$commits[0, ], - "2016-07-12 16:03:29-2016-07-12 16:06:29" = data$commits[3:5, ], - "2016-07-12 16:04:59-2016-07-12 16:06:33" = data$commits[3:8, ] + "2016-07-12 16:03:29-2016-07-12 16:06:29" = data$commits[3:7, ], + "2016-07-12 16:04:59-2016-07-12 16:06:33" = data$commits[3:10, ] ), commit.messages = list( "2016-07-12 15:58:59-2016-07-12 16:01:59" = data$commit.messages, @@ -1141,7 +1141,7 @@ patrick::with_parameters_test_that("Split a data object time-based using custom commits = list( "2016-07-12 15:00:00-2016-07-12 16:00:00" = data$commits[1, ], "2016-07-12 16:00:00-2016-07-12 16:05:00" = data$commits[2, ], - "2016-07-12 16:05:00-2016-08-08 00:00:00" = data$commits[3:8, ], + "2016-07-12 16:05:00-2016-08-08 00:00:00" = data$commits[3:10, ], "2016-08-08 00:00:00-2016-10-05 09:00:00" = data$commits[0, ] ), commit.messages = list( @@ -1380,7 +1380,7 @@ patrick::with_parameters_test_that("Split a data object time-based with equal-si commits = list( "2016-07-12 15:58:59-2016-07-12 16:01:30" = data$commits[1:2, ], "2016-07-12 16:01:30-2016-07-12 16:04:01" = data$commits[0, ], - "2016-07-12 16:04:01-2016-07-12 16:06:33" = data$commits[3:8, ] + "2016-07-12 16:04:01-2016-07-12 16:06:33" = data$commits[3:10, ] ), commit.messages = list( "2016-07-12 15:58:59-2016-07-12 16:01:30" = data$commit.messages, From 50b9b68effd49f853b8bcb335676357a854d1f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 6 Jul 2025 23:32:11 +0200 Subject: [PATCH 83/92] Fix restoring date-based order after merging additional data sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- util-data.R | 87 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/util-data.R b/util-data.R index f137a928..7f6330a2 100644 --- a/util-data.R +++ b/util-data.R @@ -361,9 +361,16 @@ ProjectData = R6::R6Class("ProjectData", ## get a vector with the column names in the right order col.names = unique(c(colnames(private$commits.unfiltered), colnames(commit.messages))) + ## store ordering + private$commits.unfiltered[["row.order"]] = seq_len(nrow(private$commits.unfiltered)) + ## merge them into the commit data private$commits.unfiltered = merge(private$commits.unfiltered, commit.messages, - by = c("commit.id", "hash"), all.x = TRUE, sort = FALSE) + by = c("commit.id", "hash"), all.x = TRUE, sort = FALSE) + + ## restore previous order because 'merge' disturbs the order + private$commits.unfiltered = private$commits.unfiltered[order(private$commits.unfiltered[["row.order"]]), ] + private$commits.unfiltered[["row.order"]] = NULL ## adjust the column order private$commits.unfiltered = private$commits.unfiltered[col.names] @@ -388,9 +395,18 @@ ProjectData = R6::R6Class("ProjectData", ## get a vector with the column names in the right order col.names = unique(c(colnames(private$commits), colnames(commit.messages))) + + ## store ordering + private$commits[["row.order"]] = seq_len(nrow(private$commits)) + ## merge them into the commit data private$commits = merge(private$commits, commit.messages, by = c("commit.id", "hash"), all.x = TRUE, sort = FALSE) + + ## restore previous order because 'merge' disturbs the order + private$commits = private$commits[order(private$commits[["row.order"]]), ] + private$commits[["row.order"]] = NULL + ## adjust the column order private$commits = private$commits[col.names] } @@ -589,17 +605,19 @@ ProjectData = R6::R6Class("ProjectData", ## only merge new data if pasta has been configured (it could also be changed to 'FALSE' in which case ## we want to just remove the columns above) if (private$project.conf$get.value("pasta")) { + ## store ordering + private$commits.unfiltered[["row.order"]] = seq_len(nrow(private$commits.unfiltered)) + ## merge PaStA data private$commits.unfiltered = merge(private$commits.unfiltered, private$pasta.commits, - by = "hash", all.x = TRUE, sort = FALSE) + by = "hash", all.x = TRUE, sort = FALSE) - ## sort by date again because 'merge' disturbs the order - private$commits.unfiltered = private$commits.unfiltered[order(private$commits.unfiltered[["date"]], decreasing = FALSE), ] + ## restore previous order because 'merge' disturbs the order + private$commits.unfiltered = private$commits.unfiltered[order(private$commits.unfiltered[["row.order"]]), ] + private$commits.unfiltered[["row.order"]] = NULL ## remove duplicated revision set ids - private$commits.unfiltered[["revision.set.id"]] = lapply(private$commits.unfiltered[["revision.set.id"]], function(rev.id) { - return(unique(rev.id)) - }) + private$commits.unfiltered[["revision.set.id"]] = lapply(private$commits.unfiltered[["revision.set.id"]], unique) } ## remove previous PaStA data @@ -609,17 +627,19 @@ ProjectData = R6::R6Class("ProjectData", ## only merge new data if pasta has been configured (it could also be changed to 'FALSE' in which case ## we want to just remove the columns above) if (private$project.conf$get.value("pasta")) { + ## store ordering + private$commits[["row.order"]] = seq_len(nrow(private$commits)) + ## merge PaStA data private$commits = merge(private$commits, private$pasta.commits, by = "hash", all.x = TRUE, sort = FALSE) - ## sort by date again because 'merge' disturbs the order - private$commits = private$commits[order(private$commits[["date"]], decreasing = FALSE), ] + ## restore previous order because 'merge' disturbs the order + private$commits = private$commits[order(private$commits[["row.order"]]), ] + private$commits[["row.order"]] = NULL ## remove duplicated revision set ids - private$commits[["revision.set.id"]] = lapply(private$commits[["revision.set.id"]], function(rev.id) { - return(unique(rev.id)) - }) + private$commits[["revision.set.id"]] = lapply(private$commits[["revision.set.id"]], unique) } logging::logdebug("update.pasta.commit.data: finished.") @@ -637,19 +657,19 @@ ProjectData = R6::R6Class("ProjectData", ## only merge new data if pasta has been configured (it could also be changed to 'FALSE' in which case ## we want to just remove the columns above) if (private$project.conf$get.value("pasta")) { + ## store ordering + private$mails.unfiltered[["row.order"]] = seq_len(nrow(private$mails.unfiltered)) + ## merge PaStA data private$mails.unfiltered = merge(private$mails.unfiltered, private$pasta.mails, by = "message.id", all.x = TRUE, sort = FALSE) - ## sort by date again because 'merge' disturbs the order - private$mails.unfiltered = private$mails.unfiltered[order(private$mails.unfiltered[["date"]], - decreasing = FALSE), ] + ## restore previous order because 'merge' disturbs the order + private$mails.unfiltered = private$mails.unfiltered[order(private$mails.unfiltered[["row.order"]]), ] + private$mails.unfiltered[["row.order"]] = NULL ## remove duplicated revision set ids - private$mails.unfiltered[["revision.set.id"]] = lapply(private$mails.unfiltered[["revision.set.id"]], - function(rev.id) { - return(unique(rev.id)) - }) + private$mails.unfiltered[["revision.set.id"]] = lapply(private$mails.unfiltered[["revision.set.id"]], unique) } ## remove previous PaStA data @@ -659,17 +679,19 @@ ProjectData = R6::R6Class("ProjectData", ## only merge new data if pasta has been configured (it could also be changed to 'FALSE' in which case ## we want to just remove the columns above) if (private$project.conf$get.value("pasta")) { + ## store ordering + private$mails[["row.order"]] = seq_len(nrow(private$mails)) + ## merge PaStA data private$mails = merge(private$mails, private$pasta.mails, by = "message.id", all.x = TRUE, sort = FALSE) - ## sort by date again because 'merge' disturbs the order - private$mails = private$mails[order(private$mails[["date"]], decreasing = FALSE), ] + ## restore previous order because 'merge' disturbs the order + private$mails = private$mails[order(private$mails[["row.order"]]), ] + private$mails[["row.order"]] = NULL ## remove duplicated revision set ids - private$mails[["revision.set.id"]] = lapply(private$mails[["revision.set.id"]], function(rev.id) { - return(unique(rev.id)) - }) + private$mails[["revision.set.id"]] = lapply(private$mails[["revision.set.id"]], unique) } logging::logdebug("update.pasta.mail.data: finished.") @@ -729,13 +751,16 @@ ProjectData = R6::R6Class("ProjectData", ## only merge new data if synchronicity has been configured (it could also be changed to 'FALSE' in ## which case we want to just remove the columns above) if (private$project.conf$get.value("synchronicity")) { + ## store ordering + private$commits.unfiltered[["row.order"]] = seq_len(nrow(private$commits.unfiltered)) + ## merge synchronicity data private$commits.unfiltered = merge(private$commits.unfiltered, private$synchronicity, by = "hash", all.x = TRUE, sort = FALSE) - ## sort by date again because 'merge' disturbs the order - private$commits.unfiltered = private$commits.unfiltered[order(private$commits.unfiltered[["date"]], - decreasing = FALSE), ] + ## restore previous order because 'merge' disturbs the order + private$commits.unfiltered = private$commits.unfiltered[order(private$commits.unfiltered[["row.order"]]), ] + private$commits.unfiltered[["row.order"]] = NULL } ## remove previous synchronicity data private$commits["synchronicity"] = NULL @@ -743,12 +768,16 @@ ProjectData = R6::R6Class("ProjectData", ## only merge new data if synchronicity has been configured (it could also be changed to 'FALSE' in ## which case we want to just remove the columns above) if (private$project.conf$get.value("synchronicity")) { + ## store ordering + private$commits[["row.order"]] = seq_len(nrow(private$commits)) + ## merge synchronicity data private$commits = merge(private$commits, private$synchronicity, by = "hash", all.x = TRUE, sort = FALSE) - ## sort by date again because 'merge' disturbs the order - private$commits = private$commits[order(private$commits[["date"]], decreasing = FALSE), ] + ## restore previous order because 'merge' disturbs the order + private$commits = private$commits[order(private$commits[["row.order"]]), ] + private$commits[["row.order"]] = NULL } ## get the caller function as a string From ae41cbd52c9d0458d4c15d18f6b9f2d7679ebd88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 29 Jul 2025 10:01:37 +0200 Subject: [PATCH 84/92] Update copyright headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-core-peripheral.R | 2 +- tests/test-data.R | 2 +- tests/test-networks-artifact.R | 2 +- tests/test-networks-bipartite.R | 2 +- tests/test-networks-commit.R | 2 +- tests/test-networks-multi.R | 2 +- tests/test-read.R | 2 +- tests/test-split-data-activity-based.R | 2 +- tests/test-split-data-time-based.R | 2 +- util-data.R | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test-core-peripheral.R b/tests/test-core-peripheral.R index 06e9cc1a..a6d524f8 100644 --- a/tests/test-core-peripheral.R +++ b/tests/test-core-peripheral.R @@ -17,7 +17,7 @@ ## Copyright 2022 by Thomas Bock ## Copyright 2019 by Christian Hechtl ## Copyright 2021 by Christian Hechtl -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## Copyright 2024-2025 by Leo Sendelbach ## All Rights Reserved. diff --git a/tests/test-data.R b/tests/test-data.R index 455d2fea..dfb4e726 100644 --- a/tests/test-data.R +++ b/tests/test-data.R @@ -19,7 +19,7 @@ ## Copyright 2021 by Johannes Hostert ## Copyright 2021 by Mirabdulla Yusifli ## Copyright 2022 by Jonathan Baumann -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. diff --git a/tests/test-networks-artifact.R b/tests/test-networks-artifact.R index d21ecada..65c87729 100644 --- a/tests/test-networks-artifact.R +++ b/tests/test-networks-artifact.R @@ -15,7 +15,7 @@ ## Copyright 2017-2019 by Claus Hunsen ## Copyright 2018 by Barbara Eckl ## Copyright 2018 by Jakob Kronawitter -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. diff --git a/tests/test-networks-bipartite.R b/tests/test-networks-bipartite.R index 796a66ed..d6cc2f0c 100644 --- a/tests/test-networks-bipartite.R +++ b/tests/test-networks-bipartite.R @@ -18,7 +18,7 @@ ## Copyright 2018 by Jakob Kronawitter ## Copyright 2018-2019 by Anselm Fehnker ## Copyright 2021 by Johannes Hostert -## Copyright 2024 by Maximilian Löffler +## Copyright 2024-2025 by Maximilian Löffler ## All Rights Reserved. diff --git a/tests/test-networks-commit.R b/tests/test-networks-commit.R index cff8849d..bd78298c 100644 --- a/tests/test-networks-commit.R +++ b/tests/test-networks-commit.R @@ -12,7 +12,7 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## ## Copyright 2024 by Leo Sendelbach -## Copyright 2024 by Maximilian Löffler +## Copyright 2024-2025 by Maximilian Löffler ## All Rights Reserved. diff --git a/tests/test-networks-multi.R b/tests/test-networks-multi.R index cbdd19da..ffa3c1a6 100644 --- a/tests/test-networks-multi.R +++ b/tests/test-networks-multi.R @@ -15,7 +15,7 @@ ## Copyright 2018 by Claus Hunsen ## Copyright 2018 by Barbara Eckl ## Copyright 2022 by Jonathan Baumann -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. diff --git a/tests/test-read.R b/tests/test-read.R index a432bbff..4fb4d849 100644 --- a/tests/test-read.R +++ b/tests/test-read.R @@ -21,7 +21,7 @@ ## Copyright 2021 by Johannes Hostert ## Copyright 2021 by Mirabdulla Yusifli ## Copyright 2022 by Jonathan Baumann -## Copyright 2022-2024 by Maximilian Löffler +## Copyright 2022-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. diff --git a/tests/test-split-data-activity-based.R b/tests/test-split-data-activity-based.R index f99984a9..63262458 100644 --- a/tests/test-split-data-activity-based.R +++ b/tests/test-split-data-activity-based.R @@ -19,7 +19,7 @@ ## Copyright 2021 by Niklas Schneider ## Copyright 2021 by Johannes Hostert ## Copyright 2022 by Jonathan Baumann -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## All Rights Reserved. context("Splitting functionality, activity-based splitting of data.") diff --git a/tests/test-split-data-time-based.R b/tests/test-split-data-time-based.R index 86f4d7df..fb7d3d0e 100644 --- a/tests/test-split-data-time-based.R +++ b/tests/test-split-data-time-based.R @@ -20,7 +20,7 @@ ## Copyright 2021 by Niklas Schneider ## Copyright 2021 by Johannes Hostert ## Copyright 2022 by Jonathan Baumann -## Copyright 2023-2024 by Maximilian Löffler +## Copyright 2023-2025 by Maximilian Löffler ## All Rights Reserved. context("Splitting functionality, time-based splitting of data.") diff --git a/util-data.R b/util-data.R index 7f6330a2..6e26d71f 100644 --- a/util-data.R +++ b/util-data.R @@ -25,7 +25,7 @@ ## Copyright 2021 by Johannes Hostert ## Copyright 2021 by Mirabdulla Yusifli ## Copyright 2022 by Jonathan Baumann -## Copyright 2022-2024 by Maximilian Löffler +## Copyright 2022-2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. From 61b538b7cf81c4b6638951bfaf051e376d0986b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sat, 9 Aug 2025 22:00:34 +0200 Subject: [PATCH 85/92] Sort author data by 'author.name' instead of 'author.id' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additionally, sort in all cases (even if 'merge' is not called) to acomodate for unsorted input data. Signed-off-by: Maximilian Löffler --- tests/test-read.R | 10 +++++----- util-read.R | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test-read.R b/tests/test-read.R index 4fb4d849..89f758d3 100644 --- a/tests/test-read.R +++ b/tests/test-read.R @@ -253,11 +253,11 @@ test_that("Read the author data.", { ## build the expected data.frame author.data.expected = data.frame( - author.id = as.character(c(4936, 4937, 4938, 4939, 4940, 4941, 4942, 4943, 4944)), - author.name = c("Thomas", "Olaf", "Björn", "udo", "Fritz fritz@example.org", "georg", "Hans", "Karl", "Max"), - author.email = c("thomas@example.org", "olaf@example.org", "bjoern@example.org", "udo@example.org", - "asd@sample.org", "heinz@example.org", "hans1@example.org", "karl@example.org", "max@example.org"), - is.bot = c(TRUE, NA, FALSE, NA, NA, NA, NA, NA, NA) + author.id = as.character(c(4938, 4940, 4941, 4942, 4943, 4944, 4937, 4936, 4939)), + author.name = c("Björn", "Fritz fritz@example.org", "georg", "Hans", "Karl", "Max", "Olaf", "Thomas", "udo"), + author.email = c("bjoern@example.org", "asd@sample.org", "heinz@example.org", "hans1@example.org", "karl@example.org", + "max@example.org", "olaf@example.org", "thomas@example.org", "udo@example.org"), + is.bot = c(FALSE, NA, NA, NA, NA, NA, NA, TRUE, NA) ) ## check the results diff --git a/util-read.R b/util-read.R index a984ca18..9dc3ded6 100644 --- a/util-read.R +++ b/util-read.R @@ -24,7 +24,7 @@ ## Copyright 2021 by Johannes Hostert ## Copyright 2021 by Mirabdulla Yusifli ## Copyright 2022 by Jonathan Baumann -## Copyright 2022-2023 by Maximilian Löffler +## Copyright 2022-2023, 2025 by Maximilian Löffler ## Copyright 2024 by Leo Sendelbach ## All Rights Reserved. @@ -525,7 +525,6 @@ read.authors = function(data.path) { authors.df = try(read.table(file, header = FALSE, sep = ";", strip.white = TRUE, encoding = "UTF-8"), silent = TRUE) - ## break if the list of authors is empty if (inherits(authors.df, "try-error") || nrow(authors.df) < 1) { logging::logerror("There are no authors available for the current environment.") @@ -542,13 +541,16 @@ read.authors = function(data.path) { bot.data = read.bot.info(data.path) if (!is.null(bot.data)) { authors.df = merge(authors.df, bot.data, by = c("author.name", "author.email"), all.x = TRUE, sort = FALSE) - authors.df = authors.df[order(authors.df[["author.id"]]), ] # re-order after read - row.names(authors.df) = seq_len(nrow(authors.df)) } else { ## if bot data is not available, add NA data, which is what would have happened ## if the file was empty authors.df[["is.bot"]] = NA } + + ## order by author name + authors.df = authors.df[order(authors.df[["author.name"]]), ] + row.names(authors.df) = seq_len(nrow(authors.df)) + ## re-order the columns authors.df = authors.df[, AUTHORS.LIST.COLUMNS] authors.df = remove.deleted.and.empty.user(authors.df) From 60eb2bdfab9e85527c24bc6ee77cae8d5b6ae1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Wed, 13 Aug 2025 22:06:46 +0200 Subject: [PATCH 86/92] Test retainment of commit order after adding commit message data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- tests/test-data.R | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test-data.R b/tests/test-data.R index dfb4e726..65941b3b 100644 --- a/tests/test-data.R +++ b/tests/test-data.R @@ -304,6 +304,10 @@ test_that("Merge commit messages to commit data", { proj.conf$update.value("commit.messages", "message") proj.data = ProjectData$new(project.conf = proj.conf) + ## + ## unfiltered commits + ## + commits = proj.data$get.commits.unfiltered() commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32710, 32714, 32707, 32708, @@ -347,6 +351,58 @@ test_that("Merge commit messages to commit data", { commit.data.expected = remove.row.names.from.data(commit.data.expected) expect_identical(commits, commit.data.expected, info = "Add commit messages with title") + + ## + ## filtered commits + ## + + commits = proj.data$get.commits() + + commit.data.expected = data.frame(commit.id = format.commit.ids(c(32712, 32713, 32708, 32708, 32711)), + date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-12 16:00:45", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:32")), + author.name = c("Björn", "Olaf", "Thomas", "Thomas", "Thomas"), + author.email = c("bjoern@example.org", "olaf@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org"), + committer.date = get.date.from.string(c("2016-07-12 15:58:59", "2016-07-20 10:00:44", "2016-07-12 16:06:20", + "2016-07-12 16:06:20", "2016-07-12 16:06:32")), + committer.name = c("Björn", "Björn", "Thomas", "Thomas", "Thomas"), + committer.email = c("bjoern@example.org", "bjoern@example.org", "thomas@example.org", "thomas@example.org", "thomas@example.org"), + hash = c("72c8dd25d3dd6d18f46e2b26a5f5b1e2e8dc28d0", "5a5ec9675e98187e1e92561e1888aa6f04faa338", + "7d5219c4ba15b8962203f0ae37f9854167914915", "7d5219c4ba15b8962203f0ae37f9854167914915", + "0a1a5c523d835459c42f33e863623138555e2526"), + changed.files = as.integer(c(1, 1, 2, 2, 1)), + added.lines = as.integer(c(1, 1, 3, 3, 1)), + deleted.lines = as.integer(c(1, 0, 1, 1, 0)), + diff.size = as.integer(c(2, 1, 2, 2, 1)), + file = c("test.c", "test.c", "test2.c", "test3.c", "test2.c"), + artifact = c("A", "A", "foo", "foo", "foo"), + artifact.type = "Feature", + artifact.diff.size = as.integer(c(1, 1, 1, 2, 1)), + title = c("Add stuff", "Add some more stuff", NA, NA, ""), + message = c("", "", NA, NA, "")) + + commits = remove.row.names.from.data(commits) + commit.data.expected = remove.row.names.from.data(commit.data.expected) + + expect_identical(commits, commit.data.expected, info = "Add commit messages with title") + + ## + ## shuffle commit message data + ## + + ## the order of commit message data should not + ## interfere with the order of the commit data + ## after merging the commit messages + + commit.message.data = proj.data$get.commit.messages() + commit.message.data = commit.message.data[sample(nrow(commit.message.data)), ] # shuffle commit message data + proj.data$set.commit.messages(commit.message.data) + + commits = proj.data$get.commits() + commits = remove.row.names.from.data(commits) + + expect_identical(commits, commit.data.expected, info = "Add commit messages with title (shuffled)") + }) test_that("Merge commit message titles to commit data", { From 5d67bb3843eb817894a088c42baa456e1d9f355e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 17 Aug 2025 21:09:59 +0200 Subject: [PATCH 87/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index 81cbebf2..d7e41f0b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -20,6 +20,8 @@ - Allow the issue data attributes `event.info.1` and `event.info.2` on network edges (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) - Add a `network.type` parameter to `get.networks` in which the caller can specify the types of networks to be constructed. This improves performance in cases where not all network types are needed, such as when building multi-networks (PR #285, bc2efd643d92da547d4677f9026540c25e730a03, e9a0c1681a0b63bd831fe7657ce25a9d50dc6e83) - Rename the `list.attributes` parameter in `add.vertex.attribute` and `split.and.add.vertex.attribute` to `flatten.values` with inverted semantics and introduce documentation for it to improve comprehensibility (PR #285, 7dab04a5251d89c9cb286452528ef8b6775a7347) +- Enhance codeface testing data by ensuring that commit ids are unique between proximity and feature data and by adding commit data that includes (1) different commits that touch the same file / function, (2) commits that are authored at the same time by different authors (PR #286, 7481099af109e1897b9e5754beb1c7da9f39ffb9, 3e53285426010cf7bf48fa23daa484f29f80ac78) +- Sort author data by `author.name` instead of `author.id` when reading it from a file (PR #286, 61b538b7cf81c4b6638951bfaf051e376d0986b3) ### Fixed @@ -27,6 +29,7 @@ - Fix a bug in network construction that could lead to edges having an unwanted `author.name.1` attribute (PR #285, 105fec1acc436378a9282c40fcae0eb0257f00be) - Ensure that POSIXct values are correctly handled in `add.vertex.attribute`, i.e., that they are not converted to numeric values (PR #285, 7dab04a5251d89c9cb286452528ef8b6775a7347, 4924ac23737dec6f915edaeb350a5cbaebbeec79) - Handle empty edges when constructing commit networks using commit-interaction data (PR #285, d5e1e4801230224e6272cd66873bd43bb7f04a00) +- Correctly retain order of commit and mail data when merging it with PAStA, synchronicity, and commit message data (PR #286, 50b9b68effd49f853b8bcb335676357a854d1f97) ## 5.0 From 35b34bf1d5049904d75122ace847debe0d1595f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 15 Sep 2025 21:32:39 +0200 Subject: [PATCH 88/92] Fix 'get.edgelist.with.timestamps' to work with listed dates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct implementations of 'get.edgelist.with.timestamp' should retain the structure of listed edges and the POSIXct type of dates. Signed-off-by: Maximilian Löffler --- tests/test-misc.R | 28 ++++++++++++++++++++++++++-- util-misc.R | 21 ++++++++++++--------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/tests/test-misc.R b/tests/test-misc.R index 47be5454..40e40159 100644 --- a/tests/test-misc.R +++ b/tests/test-misc.R @@ -24,15 +24,18 @@ test_that("Get edgelist augmented with timestamps", { + ## + ## Artifical network + ## + ## construct network - edges = list(list("A", "A"), list("D", "C"), list("C", "A"), list("B", "C")) + edges = list(list("A", "A"), list("D", "C"), list("D", "C"), list("B", "C")) timestamps = c("2016-12-07 15:30:02", "2016-08-07 15:37:02", "2016-07-12 15:59:25", "2016-07-12 15:59:59") network = igraph::make_empty_graph(n = 0, directed = TRUE) + igraph::vertices("A", "B", "C", "D") + igraph::edges(edges, relation = "mail", date = timestamps) - ## get edgelist augmented with timestamps edgelist = get.edgelist.with.timestamps(network) @@ -45,6 +48,27 @@ test_that("Get edgelist augmented with timestamps", { expect_equal(actual[["to"]], edges[[i]][[2]]) expect_equal(actual[["date"]], timestamps[i]) }) + + ## + ## Authentic network + ## + + ## make network authentic + network = igraph::set_edge_attr(network, "date", value = get.date.from.string(timestamps)) + network = convert.edge.attributes.to.list(network) + network = simplify.network(network, remove.loops = FALSE) + + ## get edgelist augmented with timestamps + edgelist = get.edgelist.with.timestamps(network) + + ## construct expected result + expected.edges = data.frame(from = c("A", "B", "D"), to = c("A", "C", "C")) + expected.edges[["date"]] = list(as.list(get.date.from.string(timestamps[1])), + as.list(get.date.from.string(timestamps[4])), + as.list(get.date.from.string(c(timestamps[2], timestamps[3])))) + + ## check correctness + expect_equal(edgelist, expected.edges, info = "Edgelist from authentic network.") }) diff --git a/util-misc.R b/util-misc.R index 97900539..a0150414 100644 --- a/util-misc.R +++ b/util-misc.R @@ -46,15 +46,18 @@ requireNamespace("lubridate") # for date conversion #' #' @return the new edgelist get.edgelist.with.timestamps = function(net) { - ## get edge list as data.frame - edges = as.data.frame(igraph::as_edgelist(net)) - colnames(edges) = c("from", "to") - ## get timestamps - dates = igraph::edge_attr(net, "date") - ## bind everything together - edges = cbind(edges, date = dates) - - return(edges) + + ## get edge list as data.frame + edges = as.data.frame(igraph::as_edgelist(net)) + colnames(edges) = c("from", "to") + + ## get timestamps + dates = igraph::edge_attr(net, "date") + + ## bind everything together + edges[["date"]] = dates + + return(edges) } From 39bf1ddc570057fcae093252d7631e13bc0b5a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Tue, 16 Sep 2025 12:01:14 +0200 Subject: [PATCH 89/92] Add 'unlist.timestamps.if.possible' parameter to convert dates to vector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the parameter is TRUE timestamps in the edgelist returned by 'get.edgelist.with.timestamps' will be into vector. Unlisting fails if the input network contains simplified edges. Signed-off-by: Maximilian Löffler --- tests/test-misc.R | 55 ++++++++++++++++++++++++++++++++++++++++------- util-misc.R | 15 +++++++++++-- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/tests/test-misc.R b/tests/test-misc.R index 40e40159..e789abc7 100644 --- a/tests/test-misc.R +++ b/tests/test-misc.R @@ -15,7 +15,7 @@ ## Copyright 2017-2018 by Claus Hunsen ## Copyright 2017-2018 by Thomas Bock ## Copyright 2023 by Thomas Bock -## Copyright 2022-2023 by Maximilian Löffler +## Copyright 2022-2023, 2025 by Maximilian Löffler ## All Rights Reserved. @@ -25,7 +25,7 @@ test_that("Get edgelist augmented with timestamps", { ## - ## Artifical network + ## Artifical network (without unlisting timestamps) ## ## construct network @@ -36,7 +36,7 @@ test_that("Get edgelist augmented with timestamps", { igraph::vertices("A", "B", "C", "D") + igraph::edges(edges, relation = "mail", date = timestamps) - ## get edgelist augmented with timestamps + ## get edgelist with timestamps edgelist = get.edgelist.with.timestamps(network) ## check correctness @@ -50,16 +50,53 @@ test_that("Get edgelist augmented with timestamps", { }) ## - ## Authentic network + ## Authentic network (without unlisting timestamps) ## - ## make network authentic + ## make network authentic (dates as POSIXct in lists) network = igraph::set_edge_attr(network, "date", value = get.date.from.string(timestamps)) network = convert.edge.attributes.to.list(network) + + ## get edgelist with timestamps (without unlisting timestamps) + edgelist = get.edgelist.with.timestamps(network, unlist.timestamps.if.possible = FALSE) + + ## check correctness + expect_equal(names(edgelist), c("from", "to", "date")) + expect_equal(nrow(edgelist), 4) + lapply(1:4, function(i) { + actual = edgelist[i, ] + expect_equal(actual[["from"]], edges[[i]][[1]]) + expect_equal(actual[["to"]], edges[[i]][[2]]) + expect_equal(actual[["date"]], list(get.date.from.string(as.list(timestamps[i])))) + }) + + ## + ## Authentic network (with unlisting timestamps) + ## + + ## get edgelist with timestamps (with unlisting timestamps) + edgelist.unlisted.if.possible = get.edgelist.with.timestamps(network, unlist.timestamps.if.possible = TRUE) + + ## check correctness + expect_equal(names(edgelist.unlisted.if.possible), c("from", "to", "date")) + expect_equal(nrow(edgelist.unlisted.if.possible), 4) + lapply(1:4, function(i) { + actual = edgelist.unlisted.if.possible[i, ] + expect_equal(actual[["from"]], edges[[i]][[1]]) + expect_equal(actual[["to"]], edges[[i]][[2]]) + expect_equal(actual[["date"]], get.date.from.string(timestamps[i])) + }) + + ## + ## Authentic network (attempt and fail to unlist timestamps) + ## + + ## simplifying edges should make unlisting timestamps impossible network = simplify.network(network, remove.loops = FALSE) - ## get edgelist augmented with timestamps - edgelist = get.edgelist.with.timestamps(network) + ## get edgelist with timestamps + edgelist = get.edgelist.with.timestamps(network, unlist.timestamps.if.possible = FALSE) + edgelist.unlisted.if.possible = get.edgelist.with.timestamps(network, unlist.timestamps.if.possible = TRUE) ## construct expected result expected.edges = data.frame(from = c("A", "B", "D"), to = c("A", "C", "C")) @@ -68,7 +105,9 @@ test_that("Get edgelist augmented with timestamps", { as.list(get.date.from.string(c(timestamps[2], timestamps[3])))) ## check correctness - expect_equal(edgelist, expected.edges, info = "Edgelist from authentic network.") + expect_equal(edgelist, expected.edges, info = "Get edgelist with timestamps.") + expect_equal(edgelist.unlisted.if.possible, expected.edges, + info = "Get edgelist with timestamps (attempt to unlist timestamps fails).") }) diff --git a/util-misc.R b/util-misc.R index a0150414..579b2be2 100644 --- a/util-misc.R +++ b/util-misc.R @@ -20,7 +20,7 @@ ## Copyright 2018-2019 by Jakob Kronawitter ## Copyright 2021 by Niklas Schneider ## Copyright 2022 by Jonathan Baumann -## Copyright 2022-2024 by Maximilian Löffler +## Copyright 2022-2025 by Maximilian Löffler ## All Rights Reserved. @@ -43,9 +43,12 @@ requireNamespace("lubridate") # for date conversion #' in order to avoid problems accessing it. #' #' @param net the given network +#' @param unlist.timestamps.if.possible whether to unlist timestamps if they are given as lists. +#' Unlisting is not possible when \code{net} contains simplified edges. +#' [default: FALSE] #' #' @return the new edgelist -get.edgelist.with.timestamps = function(net) { +get.edgelist.with.timestamps = function(net, unlist.timestamps.if.possible = FALSE) { ## get edge list as data.frame edges = as.data.frame(igraph::as_edgelist(net)) @@ -54,6 +57,14 @@ get.edgelist.with.timestamps = function(net) { ## get timestamps dates = igraph::edge_attr(net, "date") + ## unlist timestamps + if (unlist.timestamps.if.possible && is.list(dates)) { + dates.flattened = do.call(base::c, unlist(dates, recursive = FALSE)) + if (nrow(edges) == length(dates.flattened)) { + dates = dates.flattened + } + } + ## bind everything together edges[["date"]] = dates From 08bcfbae53c22691fe17d2f7897b4e5ac9c7db93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Sun, 21 Sep 2025 14:40:28 +0200 Subject: [PATCH 90/92] Temporarily fix plot printing by replacing 'ggraph::scale_edge_linetype' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As discussed in PR#289, 'graph::scale_edge_linetype' produces a scale with 'palette' = NULL. Upon printing the resulting plot (as done in 'showcase.R') this invalid palette causes the following error: "Cannot convert `x` to discrete palette" We can fix the problem temporarily by creating the scale manually through 'ggplot2::discrete_scale' and setting the palette to the default linetype palette. Signed-off-by: Maximilian Löffler --- util-plot.R | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/util-plot.R b/util-plot.R index c1381d2f..05b5112c 100644 --- a/util-plot.R +++ b/util-plot.R @@ -15,7 +15,7 @@ ## Copyright 2018 by Barbara Eckl ## Copyright 2018 by Thomas Bock ## Copyright 2020-2021, 2025 by Thomas Bock -## Copyright 2024 by Maximilian Löffler +## Copyright 2024-2025 by Maximilian Löffler ## All Rights Reserved. @@ -167,7 +167,9 @@ plot.get.plot.for.network = function(network, labels = TRUE) { end = 0.8, begin = 0.05) + ## scale edges (colors and styles) - ggraph::scale_edge_linetype(name = "Relation Types") + + ggplot2::discrete_scale(name = "Relation Types", aesthetics = "edge_linetype", palette = scales::pal_linetype()) + + ## BROKEN RIGHT NOW due to bug in scale_linetype() internally invoked by scale_edge_linetype(): + # ggraph::scale_edge_linetype(name = "Relation Types") + ggplot2::discrete_scale(name = "Relations", aesthetics = "edge_colour", palette = viridis::viridis_pal(option = "viridis", end = 0.8, begin = 0.25)) + ## BROKEN RIGHT NOW due to bug in scale_edge_colour_viridis(): From 9ae2a8e85d581bd5cf02b46c833fdb7f3289b220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20L=C3=B6ffler?= Date: Mon, 29 Sep 2025 20:43:05 +0200 Subject: [PATCH 91/92] Update 'NEWS.md' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Löffler --- NEWS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index d7e41f0b..9c4a3142 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,7 +8,7 @@ - Add the possibility to split networks that contain simplified edges (PR #278, 9798d33512dcdf50d3b88a1223fc4913a2a88eeb, 0ed437c14423c1917f1ba470e7e55db4626d380b, 67a6651b94d50cb7c2ab4a74888b0556d607b102, 98ef83158204be2a67b115cb25df5ba375cccf60, 7ec4d83fdeb308a24a350acd808941807b9511f1, 637d62ab70f098f26f241e588a99cdc49d10f56a, 2c70666f128f96a3a573f29a0cbbef14d803d193, 1cbc6fa36859d6db3a7ff4493ef19763e87d2de3, 41788ff029d038969bfc6b5773e919201c5ac595, b042c0dd08e2229514ccd25dee7a119f25b1ab45, 36d23d657f412aa1953c4773076e593273f19d8e, 402c256d9a05e4ffb297d4ea1fc25d0230787bc0, 54af2b19a112070f10d191b98b055482748426a7, 894414a4a970822b9ecd59c0b6c480860707f636, 0fe32a259ef703c2de79135bfa6932a595fdc1c5) - Add functionality for commit-message content analysis, such as NLP tools including stemming, tokenization, and lemmatization, as well as a function to search for keywords in commit messages and a function to measure the length of the messages (PR #281, 5aa4e4193f0c00095fedf961c6060a5c035ef9c6, 99f0638566c0062b987617bc3fe3ace1db7729ee, e469d3a0cf2881c378469b6ccfea9c204d13f19b, 7d8fd39f164c776921e3fb36daf79256e7be7426, ef689f71f248059cc69be4792ca14ce3b95dcac8, 6e642242a3063663bcc3c7f5cca0650dfebb6bb4, f54439486115cada08dac23864b2f7605edca9ea, dd9246b2f4506d3d58f1c1f37fc198aaaafebb0d) -- Deprecate support for R version 4.0 because of breaking dependencies (PR #281, 3dc91b155b3e0e2a55378592db448606381f902e) +- Add `unlist.timestamps.if.possible` parameter to `get.edgelist.with.timestamps` which allows callers to request a conversion of the timestamps from list to vector if possible, i.e., when there are no simplified edges in the network (PR #289, 39bf1ddc570057fcae093252d7631e13bc0b5a55) ### Changed/Improved @@ -22,6 +22,7 @@ - Rename the `list.attributes` parameter in `add.vertex.attribute` and `split.and.add.vertex.attribute` to `flatten.values` with inverted semantics and introduce documentation for it to improve comprehensibility (PR #285, 7dab04a5251d89c9cb286452528ef8b6775a7347) - Enhance codeface testing data by ensuring that commit ids are unique between proximity and feature data and by adding commit data that includes (1) different commits that touch the same file / function, (2) commits that are authored at the same time by different authors (PR #286, 7481099af109e1897b9e5754beb1c7da9f39ffb9, 3e53285426010cf7bf48fa23daa484f29f80ac78) - Sort author data by `author.name` instead of `author.id` when reading it from a file (PR #286, 61b538b7cf81c4b6638951bfaf051e376d0986b3) +- Deprecate support for R version 4.0 because of breaking dependencies (PR #281, 3dc91b155b3e0e2a55378592db448606381f902e) ### Fixed @@ -30,6 +31,7 @@ - Ensure that POSIXct values are correctly handled in `add.vertex.attribute`, i.e., that they are not converted to numeric values (PR #285, 7dab04a5251d89c9cb286452528ef8b6775a7347, 4924ac23737dec6f915edaeb350a5cbaebbeec79) - Handle empty edges when constructing commit networks using commit-interaction data (PR #285, d5e1e4801230224e6272cd66873bd43bb7f04a00) - Correctly retain order of commit and mail data when merging it with PAStA, synchronicity, and commit message data (PR #286, 50b9b68effd49f853b8bcb335676357a854d1f97) +- Fix `get.edgelist.with.timestamps` to work correctly on networks with dates in default (list) format (PR #289, 35b34bf1d5049904d75122ace847debe0d1595f7) ## 5.0 From 2ab1212523f357259c12d17e65b2c1ed0c23e255 Mon Sep 17 00:00:00 2001 From: Thomas Bock Date: Thu, 2 Oct 2025 22:57:11 +0200 Subject: [PATCH 92/92] Version 5.1 Signed-off-by: Thomas Bock --- NEWS.md | 7 ++++--- README.md | 2 +- util-networks.R | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9c4a3142..7259c744 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,7 @@ # coronet – Changelog -## unversioned +## 5.1 ### Added @@ -15,13 +15,13 @@ - For consistency reasons: Ensure that the values of edge attributes are always lists even when they represent singular values (PR #278, 6fae1843740ed8e48c89c2ee4e61f995b5d0b8f5, 416c817998540fc0b82d9959574838b571b4d6fb) - Reduce the amount of redundantly built networks by caching network data internally. This should improve the performance of building multi-networks, especially, when parts of the multi-networks have been built before (#119, PR #282, 06a814c945f0b20af842d20247126083523cde55, 4793eab02e8792b0640fad88a90018292b1b2ab9, 8ba907fff0534c6fef39bd289ab163c90b053530, 28d22902e32e93c0d4990576da2ef3de88fdffbd, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0, b30c7f2b5b0a6d12e8024fafada5490170530ebe, 1fa340d6347090a327b4c32ece705c1f700234e5, 40cd55423be7b6521e2fc35f5aa200ff0594e77c, 8fcc74439c28b1592e964dd753bfc1cd57c062be, ca348f1de8e3b4e5786a6d2726ca14e530446896, 1d233af734f79e677d3388f7c3589ce186cc3a8d, 5dd5fc18940ce9ac9598902f175193167b471966) - Internally cache commit-network data and bipartite-network data similarly to how we cache network data for author-, and artifact-networks (PR #282, PR #285, 3608214b9bcf1ac5edc0c47182993c4fcc95d8b0, aa7e3fae9f45df73054f7d9c6a96177575106c7d) -- Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) - Ensure that configured or implicitly-enforced undirectedness in partial networks is always dominant over configured or implicitly-enforced directedness. Furthermore, ensure consistency in the directedness used for edge generation and as a network attribute, especially in networks that consist of multiple partial networks such as multi-networks (PR #282, 65ead39b7b971e5a0acbaee4e787efcf194aafc4, a776caf72256200e1bfa5578106a9b53547b00e7, 257a1c8a6a9b1c3e2a72960cc4051a87950753ee, 41cff01cf141a377c96048f0645e05fb200138e9) +- Remove redundant entries from the list of allowed edge attributes and instead add `event.info.1` and `event.info.2` (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) - Allow the issue data attributes `event.info.1` and `event.info.2` on network edges (PR #282, 1b156c17f261d8b70d8d48c6cb94d3ee591559f3) - Add a `network.type` parameter to `get.networks` in which the caller can specify the types of networks to be constructed. This improves performance in cases where not all network types are needed, such as when building multi-networks (PR #285, bc2efd643d92da547d4677f9026540c25e730a03, e9a0c1681a0b63bd831fe7657ce25a9d50dc6e83) - Rename the `list.attributes` parameter in `add.vertex.attribute` and `split.and.add.vertex.attribute` to `flatten.values` with inverted semantics and introduce documentation for it to improve comprehensibility (PR #285, 7dab04a5251d89c9cb286452528ef8b6775a7347) -- Enhance codeface testing data by ensuring that commit ids are unique between proximity and feature data and by adding commit data that includes (1) different commits that touch the same file / function, (2) commits that are authored at the same time by different authors (PR #286, 7481099af109e1897b9e5754beb1c7da9f39ffb9, 3e53285426010cf7bf48fa23daa484f29f80ac78) - Sort author data by `author.name` instead of `author.id` when reading it from a file (PR #286, 61b538b7cf81c4b6638951bfaf051e376d0986b3) +- Enhance codeface testing data by ensuring that commit ids are unique between proximity and feature data and by adding commit data that includes (1) different commits that touch the same file / function, (2) commits that are authored at the same time by different authors (PR #286, 7481099af109e1897b9e5754beb1c7da9f39ffb9, 3e53285426010cf7bf48fa23daa484f29f80ac78) - Deprecate support for R version 4.0 because of breaking dependencies (PR #281, 3dc91b155b3e0e2a55378592db448606381f902e) ### Fixed @@ -33,6 +33,7 @@ - Correctly retain order of commit and mail data when merging it with PAStA, synchronicity, and commit message data (PR #286, 50b9b68effd49f853b8bcb335676357a854d1f97) - Fix `get.edgelist.with.timestamps` to work correctly on networks with dates in default (list) format (PR #289, 35b34bf1d5049904d75122ace847debe0d1595f7) + ## 5.0 ### Announcement diff --git a/README.md b/README.md index 01864702..7fc21d2b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ While using the package, we require the following infrastructure. Minimum requirement is `R` version `4.1.1`. Hence, later `R` versions also work. (Earlier `R` versions beginning from version `3.3.1` on should also work, but some packages are not available any more for these versions, so we do not test them any more in our CI pipeline.) -We currently *recommend* `R` version `4.1.1` or `4.3.0` for reliability reasons and `packrat` compatibility, but also later `R` versions should work (and are tested using our CI script). +We currently *recommend* `R` version `4.3.0` or `4.5.1` for reliability reasons and `packrat` compatibility, but also later `R` versions should work (and are tested using our CI script). #### [`packrat`](http://rstudio.github.io/packrat/) (recommended) diff --git a/util-networks.R b/util-networks.R index c5b0f732..cf2556ee 100644 --- a/util-networks.R +++ b/util-networks.R @@ -1802,8 +1802,8 @@ construct.edges.no.temporal.order = function(set, network.conf, edge.attributes, #' @param edge.list list of edges #' @param network.conf the network configuration #' @param directed whether or not the network should be directed [default: FALSE] -#' @param available.edge.attributes a named vector/list of attribute classes, with their corresponding names -#' as names on the list [default: list()] +#' @param possible.edge.attributes a named vector/list of attribute classes, with their corresponding names +#' as names on the list [default: list()] #' #' @return the built network construct.network.from.edge.list = function(vertices, edge.list, network.conf, directed = FALSE,