diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 7be58b2a9..5792a0c27 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -11,8 +11,9 @@ ## 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. 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 @@ -89,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 - diff --git a/NEWS.md b/NEWS.md index ef3ad4e25..7259c7441 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,38 @@ # coronet – Changelog +## 5.1 + +### 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, f54439486115cada08dac23864b2f7605edca9ea, dd9246b2f4506d3d58f1c1f37fc198aaaafebb0d) +- 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 + +- 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) +- 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) +- 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 + +- 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) +- 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 4552725c8..7fc21d2b3 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 content analysis](#commit-message-content-analysis) - [How-to](#how-to) - [File/Module overview](#filemodule-overview) - [Configuration classes](#configuration-classes) @@ -62,9 +63,9 @@ 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). +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) @@ -147,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 @@ -429,6 +433,10 @@ 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 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.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 In this section, we give a short example on how to initialize all needed objects and build a bipartite network. diff --git a/install.R b/install.R index 1af5c35a2..90e5db8e2 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. ## @@ -48,6 +48,9 @@ packages = c( "Matrix", "fastmap", "purrr", + "tm", + "textstem", + "SnowballC", "testthat", "patrick", "covr" @@ -76,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.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"] - 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.") } } diff --git a/showcase.R b/showcase.R index 9ab5934ad..33221f093 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,19 @@ 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 ----------------------------------------- + +## 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") + +## 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/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 35f416517..6b2db0601 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,9 @@ 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 +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 2f1476d09..040901a4d 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,10 @@ 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 +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 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/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 bbf2e3b6c..25ba4fbb0 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 e4f136f19..00a410973 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 a027c356f..a6d524f8b 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. @@ -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-misc.R b/tests/test-data-misc.R new file mode 100644 index 000000000..f490a1856 --- /dev/null +++ b/tests/test-data-misc.R @@ -0,0 +1,523 @@ +## 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-data-misc.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: Lowercase transformation", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.commit.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: Lowercase transformation, only title", { + proj.conf$update.value("commit.messages", "title") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.commit.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) + result = get.preprocessed.commit.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.commit.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) + result = get.preprocessed.commit.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) +}) + +test_that("Commit message preprocessing steps: All preprocesing", { + proj.conf$update.value("commit.messages", "message") + proj.data = ProjectData$new(proj.conf) + result = get.preprocessed.commit.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 work expected", + "")) + ## Assert + 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) + result = get.preprocessed.commit.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 work expected", + "")) + ## 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 work expect", + "")) + ## 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) +}) + +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) + 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) + 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 work expect", + "")) + ## 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")) + rownames(result) = NULL + ## Assert + 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 ")) + rownames(result) = NULL + ## 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")) + rownames(result) = NULL + ## Assert + 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) + 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")) + rownames(result) = NULL + ## 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"), c("character", "character")) + rownames(result) = NULL + ## 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 ")) + rownames(result) = NULL + ## 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")) + rownames(result) = NULL + ## 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) +}) + +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) +}) diff --git a/tests/test-data.R b/tests/test-data.R index c983946de..65941b3b6 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. @@ -304,41 +304,105 @@ 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, 32715, 32716, - 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") + + ## + ## 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", { @@ -349,36 +413,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, 32715, 32716, - 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-misc.R b/tests/test-misc.R index 47be5454f..e789abc70 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. @@ -24,16 +24,19 @@ test_that("Get edgelist augmented with timestamps", { + ## + ## Artifical network (without unlisting timestamps) + ## + ## 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 + ## get edgelist with timestamps edgelist = get.edgelist.with.timestamps(network) ## check correctness @@ -45,6 +48,66 @@ test_that("Get edgelist augmented with timestamps", { expect_equal(actual[["to"]], edges[[i]][[2]]) expect_equal(actual[["date"]], timestamps[i]) }) + + ## + ## Authentic network (without unlisting timestamps) + ## + + ## 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 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")) + 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 = "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/tests/test-networks-artifact.R b/tests/test-networks-artifact.R index 56300d5aa..65c877291 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. @@ -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 43fb347f0..d0a93b59a 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. @@ -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" @@ -457,7 +464,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 +474,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-bipartite.R b/tests/test-networks-bipartite.R index 3c6fd3b4c..d6cc2f0cf 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. @@ -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 e5c396726..bd78298c2 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. @@ -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 99f462b40..8759eb218 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. @@ -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") ) ) @@ -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) }) }) @@ -1754,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 @@ -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) }) }) @@ -2909,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-equal-constructions.R b/tests/test-networks-equal-constructions.R index e64972dac..c280285c5 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 d536cade4..9bbb53f72 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. @@ -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,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 = c(rep("cochange", 8), rep("mail", 4)), message.id = I(c(as.list(rep(NA, 8)), "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "<6784529b0802032245r5164f984l342f0f0dc94aa420@mail.gmail.com>", @@ -93,8 +92,6 @@ 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"]]) @@ -103,7 +100,8 @@ test_that("Network construction of the undirected author network with relation = ## 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,16 +199,13 @@ 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 = 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"]]) @@ -218,7 +213,8 @@ test_that("Construction of the bipartite network for the feature artifact with a ## 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)) }) @@ -257,81 +253,79 @@ 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 "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""), - 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", "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: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", 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(...)` - 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"]]) @@ -343,7 +337,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) }) @@ -386,47 +381,50 @@ 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 = 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: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")), + 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)))), - weight = c(rep(1, 30)), - type = c(rep("Bipartite", 30)), - relation = c(rep("cochange", 6), rep("issue", 24)), - issue.id = I(c(as.list(rep(NA, 6)), + 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", 8), rep("issue", 24)), + issue.id = I(c(as.list(rep(NA, 8)), "", "", "", "", "", "", "", "", "", @@ -435,12 +433,10 @@ 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(...)` - 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"]]) @@ -448,6 +444,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re 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) @@ -494,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 = 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: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)))), - weight = rep(1,22), - type = rep("Bipartite", 22), - relation = c(rep("cochange", 6), rep("mail", 16)), - message.id = I(c(as.list(rep(NA, 6)), "", + 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", 8), rep("mail", 16)), + message.id = I(c(as.list(rep(NA, 8)), "", "<1107974989.17910.6.camel@jmcmullan>", "<4cbaa9ef0802201124v37f1eec8g89a412dfbfc8383a@mail.gmail.com>", "", "", "", "", "", @@ -528,15 +526,13 @@ 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)), "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "")) ) ## 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"]]) @@ -544,6 +540,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re 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) @@ -604,21 +601,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("", "", "", "", "", "", "", "", "", @@ -628,8 +625,8 @@ 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), + 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>", @@ -639,22 +636,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[["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) @@ -707,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", "", "", "", "", "", "", "", "", "", "", @@ -725,33 +720,34 @@ 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: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)))), - weight = rep(1, 46), - type = rep("Bipartite", 46), - relation = c(rep("cochange", 6), rep("issue", 24), rep("mail", 16)), - issue.id = I(c(as.list(rep(NA, 6)), "", "", + 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", 8), rep("issue", 24), rep("mail", 16)), + issue.id = I(c(as.list(rep(NA, 8)), "", "", "", "", "", "", "", "", "", "", "", @@ -760,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>", "", "", "", "", "", @@ -769,15 +765,13 @@ 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)), "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "")) ) ## 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"]]) @@ -787,6 +781,7 @@ test_that("Construction of the multi-artifact bipartite network with artifact re 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-multi.R b/tests/test-networks-multi.R index 52770fa4f..ffa3c1a6e 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. @@ -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-networks.R b/tests/test-networks.R index 1cd093b73..d393223d5 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. @@ -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 @@ -280,6 +288,75 @@ 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(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.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.base, 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(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"), + directed = FALSE) + + 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", { ## construct network @@ -1022,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), @@ -1089,3 +1167,172 @@ 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")) + +}) + +## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / +## 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) + } + +}) + diff --git a/tests/test-read.R b/tests/test-read.R index f01d16c1c..89f758d30 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. @@ -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, 32715, 32716, - 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.") @@ -245,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/tests/test-split-data-activity-based.R b/tests/test-split-data-activity-based.R index c2043a6e1..632624589 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.") @@ -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 40381c6cd..fb7d3d0e1 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.") @@ -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, diff --git a/tests/test-split-network-time-based.R b/tests/test-split-network-time-based.R index b8b102799..8d9d0e231 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,87 @@ 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 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] +#' +#' @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) { + + ## 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 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 +#' +#' @return the partial edge +#' +#' @seealso \code{split.network.by.bins} +extract.partial.edge = function(edge, partials) { + + ## 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]][partials] + } + + ## assume equal distribution of weights across partials + else if (attr == "weight") { + partial.edge[[attr]] = length(partials) + } + } + + return(partial.edge) +} + ## * time-based ------------------------------------------------------------ ## * * time period --------------------------------------------------------- @@ -100,7 +181,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 +325,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 +416,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 +545,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), @@ -481,3 +633,150 @@ 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", "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", "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, ], c(2, 3)), + 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", { + + ## + ## 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"), + 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 as subnets 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) { + igraph::identical_graphs(s, e) + }) + expect_true(all(check.identical), info = "Network equality.") + + ## + ## splits only contain complete edges + ## + + ## 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: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, 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, ]) + ) + ## 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) { + igraph::identical_graphs(s, e) + }) + expect_true(all(check.identical), info = "Network equality.") + +}) + +test_that("Split network with arbitrarily set weight attribute", { + + ## construct network + 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:01:00", + "2025-01-01 12:01:00", "2025-01-01 12:03:00", + "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, 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, ])) + ) + ## 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) { + igraph::identical_graphs(s, e) + }) + expect_true(all(check.identical), info = "Network equality.") + +}) diff --git a/util-conf.R b/util-conf.R index 85aec34a5..281d2f3ad 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 ), @@ -898,7 +899,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-data-misc.R b/util-data-misc.R index 61d5bb840..104bc1cab 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. ## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / @@ -28,6 +29,10 @@ requireNamespace("sqldf") # for SQL-selections on data.frames 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 parallel computation #' Helper function to mask all issues in the issue data frame. #' @@ -768,3 +773,225 @@ get.issue.is.pull.request = function(proj.data) { logging::logdebug("get.issue.is.pull.request: finished") return(issue.id.to.is.pr) } + +## / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / +## 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"} +#' +#' \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{"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, 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 +#' 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 +#' [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 + ## 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(preprocessed.messages) + } + + ## create a corpus with all selected commit messages + if (proj.data$get.project.conf.entry("commit.messages") == "message") { + messages = paste(commit.message.data[["title"]], commit.message.data[["message"]]) + } else { + messages = commit.message.data[["title"]] + } + 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 ("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) + } + + ## trim leading and trailing spaces + corpus = tm::tm_map(corpus, trimws) + + ## create output dataframe + 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. +#' Preprocessing steps are always performed in the following order: +#' \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 +#' [default: NULL] +#' @param preprocessing the preprocessing steps to be executed +#' [default: 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) + ## build corpus + corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[, "preprocessed.message"])) + ## apply stemming + corpus = tm::tm_map(corpus, tm::stemDocument) + ## create output dataframe + stemmed.messages = data.frame(hash = preprocessed.messages[["hash"]], + stemmed.message = corpus$content) + return(stemmed.messages) +} + +#' 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. 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 +#' [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 + ## 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 (proj.data$get.project.conf.entry("commit.messages") == "message") { + messages = paste(commit.message.data[["title"]], commit.message.data[["message"]]) + } else { + messages = commit.message.data[["title"]] + } + tokens = lapply(messages, tm::Boost_tokenizer) + + return(tokens) +} + +#' 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"} +#' +#' @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 +#' [default: NULL] +#' @param preprocessing the preprocessing steps to be executed +#' [default: 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) + ## build corpus + corpus = tm::Corpus(tm::VectorSource(preprocessed.messages[, "preprocessed.message"])) + ## apply lemmatization + corpus = tm::tm_map(corpus, textstem::lemmatize_strings) + ## create output dataframe + lemmatized.messages = data.frame(hash = preprocessed.messages[["hash"]], + lemmatized.message = corpus$content) + return(lemmatized.messages) +} + +#' 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 \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 \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, 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 + 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, ] + } + + ## 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(parallel::mclapply(commit.message.data[["message"]], function(msg) { + match(lapply(strings, function(word) { + return (grepl(word, msg, ignore.case = ignore.case)) + })) + })), ] + return(messages) +} + + +#' 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 \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) { + ## 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) + + hashes = commit.message.data[["hash"]] + counts = unlist(lapply(tokens, length)) + messages = data.frame(hash = hashes, + count = counts) + return(messages) +} diff --git a/util-data.R b/util-data.R index f137a9280..6e26d71f6 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. @@ -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 diff --git a/util-misc.R b/util-misc.R index 97900539e..579b2be27 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,18 +43,32 @@ 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 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.edgelist.with.timestamps = function(net, unlist.timestamps.if.possible = FALSE) { + + ## 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") + + ## 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 + + return(edges) } diff --git a/util-networks-covariates.R b/util-networks-covariates.R index 6db374a30..e57b41257 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) } diff --git a/util-networks.R b/util-networks.R index 73d7ab6a1..cf2556ee4 100644 --- a/util-networks.R +++ b/util-networks.R @@ -55,11 +55,13 @@ 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", type = "first", - relation = function(relation) sort(unique(relation)), ## commit data changed.files = "sum", @@ -68,8 +70,16 @@ EDGE.ATTR.HANDLING = list( diff.size = "sum", artifact.diff.size = "sum", - ## everything else - "concat" + ## 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) + } + return(attr) + } ) @@ -84,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 ---------------------------------------------------------- @@ -114,17 +132,20 @@ 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.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.mail.data = NULL, + artifact.network.issue.data = NULL, + artifact.network.commit.interaction.data = NULL, + artifact.network.callgraph.data = NULL, + commit.network.cochange.data = NULL, + commit.network.commit.interaction.data = NULL, + bipartite.relations = NULL, ## * * relation-to-vertex-kind mapping ----------------------------- @@ -166,19 +187,120 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", return(data.sources) }, + ## * * helper functions -------------------------------------------- + + #' 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 + #' @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, possible.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) { + 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 + 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", possible.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) + }, + + #' 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. #' 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 - 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 @@ -198,7 +320,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") ) @@ -212,29 +334,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"]], + possible.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() { + get.author.network.commit.interaction = function(directed) { + 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"]])) @@ -251,87 +377,86 @@ 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, + possible.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. #' 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.") ## 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 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") ) - ## 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"]], + possible.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() { + get.author.network.issue = function(directed) { 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 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") ) - ## 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"]], + possible.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 ------------------------------------------- @@ -340,14 +465,14 @@ 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.") ## 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 @@ -356,90 +481,107 @@ 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" ) - ## 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, + possible.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"]], + get.artifact.network.commit.interaction = function(directed) { + + 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 = 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, + possible.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. @@ -447,14 +589,14 @@ 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.") ## 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 @@ -518,25 +660,33 @@ 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"), + possible.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. #' 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.") ## 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 @@ -545,31 +695,32 @@ 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, + possible.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. #' 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.") ## 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")) { @@ -592,7 +743,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", ] @@ -638,7 +789,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(artifacts.net.data.raw[["issue.id"]]) edge.list = data.frame() # edges in artifact networks can not have the 'artifact' attribute but should instead have @@ -647,7 +798,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 @@ -668,40 +821,31 @@ 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, + possible.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. #' #' @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.") ## 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 @@ -714,40 +858,40 @@ 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" - 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, + possible.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, #' 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.") ## 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 @@ -756,25 +900,23 @@ 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" ) - ## 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"]], + possible.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 ------------------------------------------ @@ -790,6 +932,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) @@ -805,6 +953,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) @@ -844,15 +993,18 @@ 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.mail.data = NULL + private$author.network.issue.data = NULL + private$author.network.commit.interaction.data = NULL + private$artifact.network.cochange.data = NULL + private$artifact.network.mail.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$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() @@ -922,26 +1074,36 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", get.author.network = function() { logging::loginfo("Constructing author network.") - ## construct network relations = private$network.conf$get.value("author.relation") - networks = lapply(relations, function(relation) { - network = switch( + directed = private$determine.directedness("author") + + ## 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) ) ## set edge attributes on all edges - igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = 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 = directed + ) ## add all missing authors to the network if wanted if (private$network.conf$get.value("author.all.authors")) { @@ -978,7 +1140,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(net, "range") = private$proj.data$get.range() } - net = convert.edge.attributes.to.list(net) return(net) }, @@ -990,28 +1151,38 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("artifact.relation") - networks = lapply(relations, function(relation) { - network = switch( + directed = private$determine.directedness("artifact") + + 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)) ) ## set edge attributes on all edges - igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = 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 = directed + ) ## set vertex and edge attributes for identifaction igraph::V(net)$type = TYPE.ARTIFACT @@ -1027,7 +1198,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(net, "range") = private$proj.data$get.range() } - net = convert.edge.attributes.to.list(net) return(net) }, @@ -1039,21 +1209,31 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", ## construct network relations = private$network.conf$get.value("commit.relation") - networks = lapply(relations, function(relation) { - network = switch( + directed = private$determine.directedness("commit") + + 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)) ) ## set edge attributes on all edges - igraph::E(network)$type = TYPE.EDGES.INTRA - igraph::E(network)$relation = 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 = directed + ) ## set vertex and edge attributes for identifaction igraph::V(net)$kind = TYPE.COMMIT @@ -1070,7 +1250,6 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(net, "range") = private$proj.data$get.range() } - net = convert.edge.attributes.to.list(net) return(net) }, @@ -1078,9 +1257,10 @@ 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$network.conf$get.value("author.directed") + directed = private$determine.directedness("author") vertex.data = lapply(bipartite.relation.data, function(net.to.net) { @@ -1116,13 +1296,16 @@ 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)) } ## 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]] @@ -1130,9 +1313,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( @@ -1140,7 +1323,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 @@ -1155,7 +1338,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 @@ -1178,34 +1361,44 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", } network = convert.edge.attributes.to.list(network) + return(network) }, - #' Get all networks as list. - #' Build unification to avoid null-pointers. + #' Get various networks in a list. + #' + #' @param network.type the type(s) of network(s) to be constructed + #' [default: c("author", "artifact", "commit", "bipartite")] #' - #' @return all networks in a list - get.networks = function() { - logging::loginfo("Constructing all networks.") + #' @return networks in a list + get.networks = function(network.type = c("author", "artifact", "commit", "bipartite")) { + + 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() + } + + return(networks) }, #' Get the multi network. @@ -1216,9 +1409,23 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", get.multi.network = function() { logging::loginfo("Constructing 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)) + ## construct the network parts we need for the multi network - networks = self$get.networks() - authors.to.artifacts = networks[["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.net = networks[["authors.net"]] igraph::V(authors.net)$kind = TYPE.AUTHOR artifacts.net = networks[["artifacts.net"]] @@ -1302,6 +1509,7 @@ NetworkBuilder = R6::R6Class("NetworkBuilder", attr(u, "range") = private$proj.data$get.range() } + u = convert.edge.attributes.to.list(u) return(u) } @@ -1365,7 +1573,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")) + } } } @@ -1376,47 +1586,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 )) @@ -1477,6 +1689,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 @@ -1588,12 +1802,12 @@ 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, - available.edge.attributes = list()) { + possible.edge.attributes = list()) { logging::logdebug("construct.network.from.edge.list: starting.") logging::loginfo("Construct network from edges.") @@ -1623,10 +1837,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 @@ -1644,14 +1859,27 @@ 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 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}) -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) @@ -1665,9 +1893,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 @@ -1677,6 +1905,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 can handle this + } + logging::logdebug("merge.network.data: finished.") return(list( vertices = vertices, @@ -1700,23 +1933,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( @@ -1736,12 +1962,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)) { @@ -1763,10 +1989,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, @@ -1792,8 +2018,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) @@ -1801,17 +2027,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]) { - extra.edge.attributes[[attr]] = as.list(extra.edge.attributes[[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) { @@ -1842,7 +2062,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") @@ -1947,25 +2167,58 @@ 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? + net = convert.edge.attributes.to.list(net) + return(net) }) ## merge the simplified networks @@ -1973,6 +2226,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 @@ -2197,12 +2451,62 @@ 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) } 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 ---------------------------------------------------------- diff --git a/util-plot.R b/util-plot.R index c1381d2fa..05b5112cf 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(): diff --git a/util-read.R b/util-read.R index a984ca183..9dc3ded6e 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) diff --git a/util-split.R b/util-split.R index 31da7ee33..2236eda55 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,11 @@ 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) + 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 } @@ -709,7 +710,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 @@ -877,19 +878,121 @@ 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) + + ## 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) + } + } + + ## 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 = FALSE + ## 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 ] - ## create network based on the current set of edges - g = igraph::subgraph_from_edges(network, edges, delete.vertices = remove.isolates) - return(g) + + ## empty edge data + subnet.edges = list( + 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 belongs completely to the current bin + if (all(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.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 (!all(which.partials)) { + ignore.numeric.attrs = TRUE + } + + ## 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.partials] + } + + ## assume equal distribution of weights across partials + else if (attr == "weight") { + partial.edge[[attr]] = sum(which.partials) + } + } + + ## 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]) + } + } + + ## 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 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) } + logging::logdebug("split.network.by.bins: finished.") return(nets) } @@ -1167,34 +1270,53 @@ 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 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 + factors = factor(dates.breaks.chr[intervals], levels = dates.breaks.chr) + return(factors) + }) + + } 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.")