Foundry 单元测试

news2024/11/8 9:26:13

安装 Foundry

如果你还没有安装 Foundry,请按照此处的说明进行操作:Foundry 安装

Foundry Hello World

只需运行以下命令,它将为你设置环境,创建测试并运行它们。(当然,这假设你已经安装了 Foundry)。

forge init
forge test

Solidity 测试最佳实践

无论使用何种框架,solidity 单元测试的质量取决于三个因素:

  • 行覆盖率
  • 分支覆盖率,以及
  • 完全定义的状态转换。

通过理解每一个因素,我们可以说明为何我们专注于 Foundry API 的某些方面。

当然,不可能为所有可能的输入输出范围编写文档。然而,测试质量通常与行覆盖率、分支覆盖率和定义的状态转换相关。在我们的另一篇文章中,我们已经记录了如何使用 Foundry 测量行和分支覆盖率 。我们将在此解释这三个度量指标的重要性:

1. 行覆盖率

行覆盖率就是它字面上的意思。如果代码行在测试中未被执行,则行覆盖率不是 100%。如果代码行从未被执行过,你无法确定它是否按预期工作或会抛出异常。在智能合约中没有不实现 100%行覆盖率的理由。如果你在编写代码,这意味着你期望它在未来的某个时候被执行,那么为什么不对其进行测试呢?

2. 分支覆盖率

即使每一行都被执行了,也不意味着每一种智能合约业务逻辑的变化都被测试了。

考虑以下函数:

function changeOwner(address newOwner) external {
    require(msg.sender == owner, "onlyOwner");
    owner = newOwner;
}

如果通过调用此地址的所有者来测试它,你将获得 100%的行覆盖率,但不会获得 100%的分支覆盖率。这是因为 require 语句和所有者分配都被执行了,但 require 抛出异常的情况没有被测试。

这里是一个更微妙的例子。

// @notice anyone can pay off someone else's loan
// @param debtor the person who's loan the sender is making a payment for
function payDownLoan(address debtor) external payable {
    uint256 loanAmount = loanAmounts[debtor];
    require(loanAmount > 0, "no such loan");

    if (msg.value >= debtAmount) {
        loanAmounts[debtor] = 0;
        emit LoanFullyRepaid(debtor);
    } else {
        emit LoanPayment(debtor, debtAmount, msg.value);
        loanAmount -= msg.value;
    }

    if (msg.value > loanAmount) {
        msg.sender.call{value: msg.value - loanAmount}("");
    }
}

在这种情况下有多少个分支需要测试?

  1. 贷款为零的情况
  2. 某人支付少于贷款金额的情况
  3. 某人支付正好等于贷款金额的情况
  4. 某人支付超过贷款金额的情况

通过发送多于或少于贷款金额的以太币,可以在此测试中获得 100%的行覆盖率。这将执行 if else 语句的两个分支以及最后的 if 语句但这不会测试贷款正好付清为零的 else 语句。

你的函数分支越多,单元测试它们就越困难。技术术语为圈复杂度 。

3. 完全定义的状态转换

高质量的 solidity 单元测试尽可能详细地记录状态转换。状态转换包括:

  • 存储变量的改变
  • 合约的部署或自毁
  • 以太余额的变化
  • 事件的触发,带有某些消息
  • 交易的回退,带有某些错误消息

如果函数执行了这些动作,修改状态的确切方式应该在单元测试中被捕获,任何偏差都应导致回退。这样,任何意外的修改,无论多么微小,都会自动被捕捉。回到之前的例子,应测试哪些状态转换?

  • 合约中的 Ether 增加了借款人偿还贷款的等量
  • 跟踪贷款金额的存储变量按预期金额减少
  • 当发送者为不存在的贷款付款时,出现预期的错误消息
  • 触发相应的事件和相关消息

如果智能合约的业务逻辑发生变化,测试应失败。在其他领域,这通常被认为是一个“脆弱”的单元测试。它可能会减慢源代码的迭代速度。但 Solidity 代码通常是一次编写且永不更改,因此这对智能合约测试来说不是问题。

4. 单元测试最佳实践结论

在记录 Foundry 单元测试工作原理之前,为什么我们要覆盖所有这些?因为这可以帮助我们隔离大多数情况下会使用的高影响测试工具。Foundry 的功能非常广泛,但多数测试用例中只会用到一小部分。

Foundry 断言

为了确保状态转换确实发生,你将需要断言。让我们从你调用forge init后 Foundry 提供的默认测试文件开始。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Counter.sol";

contract CounterTest is Test {
    Counter public counter;

    function setUp() public {
        counter = new Counter();
        counter.setNumber(0);
    }

    function testIncrement() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }

    function testSetNumber(uint256 x) public {
        counter.setNumber(x);
        assertEq(counter.number(), x);
    }
}

setUp() 函数部署你正在测试的合约(以及生态系统中的其他合约)。

任何以test开头的函数将被执行为单元测试。不以test开头的函数将不会被执行,除非它们被testsetUp函数调用。

这里 可以找到你可以使用的断言。

你最常用的有:

  • assertEq,断言相等
  • assertLt,断言小于
  • assertLe,断言小于或等于
  • assertGt,断言大于
  • assertGe,断言大于或等于
  • assertTrue,断言为真

前两个传递给 assert 的参数是比较内容,但你也可以添加一个作为第三个参数的帮助错误信息,你应该总是这样做(尽管默认示例没有显示)。以下是推荐的写断言的方式:

function testIncrement() public {
        counter.increment();
        assertEq(counter.number(), 1, "expect x to equal to 1");
}

function testSetNumber(uint256 x) public {
        counter.setNumber(x);
        assertEq(counter.number(), x, "x should be setNumber");
}

使用 Foundry vm.prank 修改 msg.sender

Foundry 更有趣的方法来改变发送者(账户或钱包)是 vm.prank API(Foundry 称之为作弊码)。

这是一个最小的示例

function testChangeOwner() public {
    vm.prank(owner);
    contractToTest.changeOwner(newOwner);
    assertEq(contractToTest.owner(), newOwner);
}

vm.prank 仅对紧随其后的事务有效。如果你想使用同一个地址进行一系列交易,请使用 vm.startPrank 并在结束后使用 vm.stopPrank

function testMultipleTransactions() public {
    vm.startPrank(owner);
    // 表现为所有者
    vm.stopPrank();
}

在 Foundry 中定义账户和地址

上面的 owner 变量可以用几种方式定义:

// 将十进制转换为地址创建的地址
address owner = address(1234);

// vitalik 的地址
address owner = 0x0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;

// 从已知私钥创建一个地址;
address owner = vm.addr(privateKey);

// 创建一个攻击者
address hacker = 0x00baddad;

msg.sender 和 tx.origin 的恶作剧

在上述示例中,msg.sender 被更改。如果你想同时控制 tx.origin 和 msg.sendervm.prank 和 vm.startPrank 都可以选择性地接受两个参数,其中第二个参数是 tx.origin

vm.prank(msgSender, txOrigin);

依赖于 tx.origin 通常是一个坏习惯,所以你很少需要使用带两个参数版本的 vm.prank

检查余额

当你转移以太币时,你应该测量余额是否按预期变化。值得庆幸的是,在 Foundry 中检查余额很容易,因为它是用 Solidity 编写的。

考虑这个合约:

contract Deposit {

    event Deposited(address indexed);

    function buyerDeposit() external payable {
        require(msg.value == 1 ether, "incorrect amount");
        emit Deposited(msg.sender);
    }

    // 逻辑的其他部分
}

测试函数如下所示。

function testBuyerDeposit() public {
    uint256 balanceBefore = address(depositContract).balance;
    depositContract.buyerDeposit{value: 1 ether}();
    uint256 balanceAfter = address(depositContract).balance;

    assertEq(balanceAfter - balanceBefore, 1 ether, "expect increase of 1 ether");
}

请注意,我们没有测试买家发送的金额不是 1 以太币的情况,这会导致回滚。我们将在下一节讨论测试回滚。

使用 vm.expectRevert 预计回滚

当前形式的上述测试的问题在于,你可以删除 require 语句,测试仍然会通过。让我们改进测试,以使删除 require 语句会导致测试失败。

function testBuyerDepositWrongPrice() public {
    vm.expectRevert("incorrect amount");
    depositContract.deposit{value: 1 ether + 1 wei}();

    vm.expectRevert("incorrect amount");
    depositContract.deposit{value: 1 ether - 1 wei}();
}

请注意,必须在我们预计要回滚的函数调用之前立即调用 vm.expectRevert。现在,如果我们删除 require 语句,它将回滚,因此我们更好地模拟了智能合约的预期功能。

测试自定义错误

如果我们使用自定义错误而不是 require 语句,测试回滚的方法如下:

contract CustomErrorContract {
    error SomeError(uint256);

    function revertError(uint256 x) public pure {
        revert SomeError(x);
    }
}

测试文件如下所示:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/RevertCustomError.sol";

contract CounterTest is Test {
    CustomErrorContract public customErrorContract;
    error SomeError(uint256);

    function setUp() public {
        customErrorContract = new CustomErrorContract();
    }

    function testRevert() public {
        // 5 是一个任意示例
        vm.expectRevert(abi.encodeWithSelector(SomeError.selector, 5));
        customErrorContract.revertError(5);
    }
}

在我们的示例中,我们创建了一个参数化的自定义错误。为了使测试通过,参数需要等于在回滚期间实际使用的参数。

使用 vm.expectEvent 测试日志和事件

虽然 solidity 事件 不会改变智能合约的功能,但错误地实现它们会破坏读取智能合约状态的客户端应用程序。为了确保我们的事件按预期工作,我们可以使用 vm.expectEmit。这个 API 的行为相当反直觉,因为你必须在测试中发出事件,以确保它在智能合约中工作。

这是一个最小的示例。

function testBuyerDepositEvent() public {
    vm.expectEmit();
    emit Deposited(buyer);

    depositContract.deposit{value: 1 ether}();
}

使用 vm.warp 调整 block.timestamp

现在让我们考虑一个时间锁定的提现。卖方可以在 3 天后提现付款。

contract Deposit {

    address public seller;
    mapping(address => uint256) public depositTime;

    event Deposited(address indexed);
    event SellerWithdraw(address indexed, uint256 indexed);

    constructor(address _seller) {
        seller = _seller;
    }

    function buyerDeposit() external payable {
        require(msg.value == 1 ether, "incorrect amount");
        uint256 _depositTime = depositTime[msg.sender];
        require(_depositTime == 0, "already deposited");
        depositTime[msg.sender] = block.timestamp;

        emit Deposited(msg.sender);
    }

    function sellerWithdraw(address buyer) external {
        require(msg.sender == seller, "not the seller");
        uint256 _depositTime = depositTime[buyer];
        require(_depositTime != 0, "buyer did not deposit");
        require(block.timestamp - _depositTime > 3 days, "refund period not passed");
        delete depositTime[buyer];

        emit SellerWithdraw(buyer, block.timestamp);
        (bool ok, ) = msg.sender.call{value: 1 ether}("");
        require(ok, "seller did not withdraw");
    }
}

我们添加了许多需要测试的功能,但现在让我们重点放在时间方面。

我们想测试卖方在存款后的 3 天内不能提取资金。(显然缺少一个买方在该时间窗口前提取的函数,但我们稍后会讨论)。

请注意,block.timestamp 默认从 1 开始。这不是一个实际的测试数字,因此我们应该首先转换到当前日期。

可以使用 vm.warp(x) 来实现这个功能,但我们可以更讲究地使用修饰符。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Deposit.sol";

contract DepositTest is Test {
    Deposit public deposit;
    Deposit public faildeposit;
    address constant SELLER = address(0x5E11E7);
    //address constant Rejector = address(RejectTransaction);
    RejectTransaction private rejector;

    event Deposited(address indexed);
    event SellerWithdraw(address indexed, uint256 indexed);

    function setUp() public {
        deposit = new Deposit(SELLER);
        rejector = new RejectTransaction();
        faildeposit = new Deposit(address(rejector));
    }

    modifier startAtPresentDay() {
        vm.warp(1680616584);
        _;
    }

    address public buyer = address(this); // DepositTest 合约即为“买家”
    address public buyer2 = address(0x5E11E1); // 随机地址
    address public FakeSELLER = address(0x5E1222); // 随机地址

    function testDepositAmount() public startAtPresentDay {
        // 此测试检查买家只能存入 1 ether
        vm.startPrank(buyer);
        vm.expectRevert();
        deposit.buyerDeposit{value: 1.5 ether}();
        vm.expectRevert();
        deposit.buyerDeposit{value: 2.5 ether}();
        vm.stopPrank();
    }
}

使用 vm.roll 调整 block.number

如果你想在 Foundry 中调整区块号 (block.number),使用

vm.roll(blockNumber)

来改变区块号。要向前移动一定数量的区块,请执行以下操作

vm.roll(block.number() + numberOfBlocks)

添加额外的测试

为了完整性,让我们为其余的功能编写单元测试。一些额外的功能需要为存款功能进行测试:

  • 公共变量 depositTime 与交易时间匹配
  • 用户不能重复存款

以及卖家功能的测试:

  • 卖家不能为不存在的地址提款
  • 买家的条目被删除(这允许买家重新购买)
  • 触发 SellerWithdraw 事件
  • 合约的余额减少 1 ether
  • 不是卖家的地址调用 sellerWithdraw 会被回滚
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Deposit.sol";

contract DepositTest is Test {
    Deposit public deposit;
    Deposit public faildeposit;
    address constant SELLER = address(0x5E11E7);
    //address constant Rejector = address(RejectTransaction);
    RejectTransaction private rejector;

    event Deposited(address indexed);
    event SellerWithdraw(address indexed, uint256 indexed);

    function setUp() public {
        deposit = new Deposit(SELLER);
        rejector = new RejectTransaction();
        faildeposit = new Deposit(address(rejector));
    }

    modifier startAtPresentDay() {
        vm.warp(1680616584);
        _;
    }

    address public buyer = address(this); // DepositTest 合约即为“买家”
    address public buyer2 = address(0x5E11E1); // 随机地址
    address public FakeSELLER = address(0x5E1222); // 随机地址

    function testDepositAmount() public startAtPresentDay {
        // 此测试检查买家只能存入 1 ether
        vm.startPrank(buyer);
        vm.expectRevert();
        deposit.buyerDeposit{value: 1.5 ether}();
        vm.expectRevert();
        deposit.buyerDeposit{value: 2.5 ether}();
        vm.stopPrank();
    }

    function testBuyerDepositSellerWithdrawAfter3days() public startAtPresentDay {
        // 此测试检查买家存款后 3 天,卖家能够提款

        // 买家存款 1 ether
        vm.startPrank(buyer); // msg.sender == buyer
        deposit.buyerDeposit{value: 1 ether}();
        assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // 检查合约的余额是否增加
        vm.stopPrank();

        // 三天后卖家提款
        vm.startPrank(SELLER); // msg.sender == SELLER
        vm.warp(1680616584 + 3 days + 1 seconds);
        deposit.sellerWithdraw(address(this));
        assertEq(address(deposit).balance, 0 ether, "Contract balance did not decrease"); // 检查合约的余额是否减少
    }

    function testBuyerDepositSellerWithdrawBefore3days() public startAtPresentDay {
        // 此测试检查买家存款后 3 天,卖家能够提款

        // 买家存款 1 ether
        vm.startPrank(buyer); // msg.sender == buyer
        deposit.buyerDeposit{value: 1 ether}();
        assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // 检查合约的余额是否增加
        vm.stopPrank();

        // 三天前卖家提款
        vm.startPrank(SELLER); // msg.sender == SELLER
        vm.warp(1680616584 + 2 days);
        vm.expectRevert(); // 预期会回滚
        deposit.sellerWithdraw(address(this));
    }

    function testdepositTimeMatchesTimeofTransaction() public startAtPresentDay {
        // 此测试检查公共变量 depositTime 是否与交易时间匹配

        vm.startPrank(buyer); // msg.sender == buyer
        deposit.buyerDeposit{value: 1 ether}();
        // 检查它是否存入于正确的时间
        assertEq(
            deposit.depositTime(buyer),
            1680616584, // startAtPresentDay 的时间
            "Time of Deposit Doesnt Match"
        );
        vm.stopPrank();
    }

    function testUserDepositTwice() public startAtPresentDay {
        // 此测试检查用户不能重复存款 

        vm.startPrank(buyer); // msg.sender == buyer
        deposit.buyerDeposit{value: 1 ether}();

        vm.warp(1680616584 + 1 days); // 一天后...
        vm.expectRevert();
        deposit.buyerDeposit{value: 1 ether}(); // 应该回滚因为未到 3 天
    }

    function testNonExistantContract() public startAtPresentDay {
        // 此测试检查卖家不能为不存在的地址提款

        vm.startPrank(SELLER); // msg.sender == SELLER
        vm.expectRevert();
        deposit.sellerWithdraw(buyer); 
    }

    function testBuyerBuysAgain() public startAtPresentDay {
        // 此测试检查买家的条目是否被删除(这允许买家重新购买)

        vm.startPrank(buyer); // msg.sender == buyer
        deposit.buyerDeposit{value: 1 ether}();
        vm.stopPrank();

        // 卖家提款
        vm.warp(1680616584 + 3 days + 1 seconds);
        vm.startPrank(SELLER); // msg.sender == SELLER
        deposit.sellerWithdraw(buyer);
        vm.stopPrank();
// 检查 depositTime[buyer] == 0
assertEq(deposit.depositTime(buyer), 0, "买家的条目未被删除");

// 买家再次存款
vm.startPrank(buyer); // msg.sender == buyer
vm.expectEmit();
emit Deposited(buyer);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
}

function testSellerWithdrawEmitted() public startAtPresentDay {
// 此测试检查 SellerWithdraw 事件是否被触发

// buyer2 存款
vm.deal(buyer2, 1 ether); // msg.sender == buyer2
vm.startPrank(buyer2);
vm.expectEmit(); // 检查 Deposited 事件
emit Deposited(buyer2);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();

vm.warp(1680616584 + 3 days + 1 seconds); // 3 天 1 秒后...

// 卖家提款 + 检查 SellerWithdraw 事件是否被触发
vm.startPrank(SELLER); // msg.sender == SELLER
vm.expectEmit(); // 期望 SellerWithdraw 事件被触发
emit SellerWithdraw(buyer2, block.timestamp);
deposit.sellerWithdraw(buyer2);
vm.stopPrank();
}

function testFakeSeller2Withdraw() public startAtPresentDay {
// 买家存款
vm.startPrank(buyer);
vm.deal(buyer, 2 ether); // 该合约地址是买家
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(deposit).balance, 1 ether, "以太存款失败");

vm.warp(1680616584 + 3 days + 1 seconds); // 3 天 1 秒后...

vm.startPrank(FakeSELLER); // msg.sender == FakeSELLER
vm.expectRevert();
deposit.sellerWithdraw(buyer);
vm.stopPrank();
}

function testRejectedWithdrawl() public startAtPresentDay {
// 此测试检查买家的条目是否被删除(这允许买家再次购买)

vm.startPrank(buyer); // msg.sender == buyer
faildeposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(faildeposit).balance, 1 ether, "断言失败");

vm.warp(1680616584 + 3 days + 1 seconds); // 3 天 1 秒后...

vm.startPrank(address(rejector)); // msg.sender == rejector
vm.expectRevert();
faildeposit.sellerWithdraw(buyer);
vm.stopPrank();
}

测试失败的以太转账

测试买家提款需要额外的技巧来获得完整的行覆盖率。以下是我们正在测试的代码片段,我们将在上面的代码中解释 Rejector 合约。

function buyerWithdraw() external {
    uint256 _depositTime = depositTime[msg.sender];
    require(_depositTime != 0, "sender did not deposit");
    require(block.timestamp - _depositTime <= 3 days);

    emit BuyerRefunded(msg.sender, block.timestamp);

    // 这是我们正在测试的分支
    (bool ok,) = msg.sender.call{value: 1 ether}("");
    require(ok, "Failed to withdraw");
}

为了测试 require(ok…) 的失败条件,我们需要让以太转账失败。测试通过创建一个调用 buyerWithdraw 函数的智能合约来实现这一点,但其 receive 函数设置为 revert

Foundry 模糊测试

虽然我们可以指定一个不是卖家的任意地址来测试未授权地址提款的 revert,但尝试许多不同的值更令人放心。

如果我们为测试函数提供参数,Foundry 将尝试许多不同的参数值。为了防止它使用不适用于测试用例的参数(例如当地址被授权时),我们将使用 vm.assume。以下是如何测试未授权卖家的卖家提款。

// notSeller 将被随机选择
function testInvalidSellerAddress(address notSeller) public {
    vm.assume(notSeller != seller);

    vm.expectRevert("not the seller");
    depositContract.sellerWithdraw(notSeller);
}

以下是所有的状态转换

  • 合约的 balance 减少了 1 ether
  • BuyerRefunded 事件被触发
  • 买家可以在三天内退款

以下是需要测试的分支

  • 买家不能在 3 天后提款
  • 买家如果从未存款则不能提款

Console.log Foundry

要在 Foundry 中使用 console.log,请导入以下内容

import "forge-std/console.sol";

并使用以下命令运行测试

forge test -vv

测试签名

请参阅我们关于 solidity 签名验证 的教程,因此我们建议你参考该教程。

Solidity 测试内部函数


请参阅我们关于 solidity 测试内部函数 的教程。

使用 vm.deal 和 vm.hoax 设置地址余额

作弊码 vm.hoax 允许你同时恶作剧一个地址并设置其余额。

vm.hoax(addressToPrank, balanceToGive);
// 下一个调用是 addressToPrank 的恶作剧

vm.deal(alice, balanceToGive);

Foundry 的一些常见错误

在接收以太时没有回退函数

如果你正在测试从合约中提取以太,它将被发送到运行测试的合约。Foundry 测试本身是一个智能合约,如果你将以太发送到没有 fallback 或 receive 函数的智能合约,则交易将失败。确保在合约中有一个 fallback 或 receive 函数。

在接收代币时没有 onERC…Received

同样,ERC-721 safeTransferFrom 和 ERC-1155 transferFrom 在将代币发送到没有适当传输钩子函数的智能合约时会回滚。如果你想测试将 NFT(或类似 ERC777 的代币)转移给自己,你需要将其添加到测试中。

总结

  • 目标是 100% 的行和分支覆盖率
  • 完全定义预期的状态转换
  • 在断言中使用错误消息

了解更多

要了解超越单元测试和基本模糊测试的高级 solidity 测试,https://t.me/gtokentool。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2235718.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jenkins声明式Pipeline流水线语法示例

系列文章目录 docker搭建Jenkins2.346.3版本及常用工具集成配置(ldap、maven、ansible、npm等) docker安装低版本的jenkins-2.346.3,在线安装对应版本插件失败的解决方法 文章目录 系列文章目录jenkins流水线基础1、pipeline1.1、什么是pipeline&#xff1f;1.2、为什么使用pi…

【NLP】使用 SpaCy、ollama 创建用于命名实体识别的合成数据集

命名实体识别 (NER) 是自然语言处理 (NLP) 中的一项重要任务&#xff0c;用于自动识别和分类文本中的实体&#xff0c;例如人物、位置、组织等。尽管它很重要&#xff0c;但手动注释大型数据集以进行 NER 既耗时又费钱。受本文 ( https://huggingface.co/blog/synthetic-data-s…

【数据集】【YOLO】【目标检测】道路裂缝数据集 5466 张,YOLO/VOC格式标注!

数据集介绍 【数据集】道路裂缝数据集 5466 张&#xff0c;目标检测&#xff0c;包含YOLO/VOC格式标注。数据集中包含一种分类&#xff0c;检测范围城市道路裂缝、高速道路裂缝、乡村道路裂缝。 戳我头像获取数据&#xff0c;或者主页私聊博主哈~ 一、数据概述 道路裂缝检测…

C++用string实现字符串相加

. - 力扣&#xff08;LeetCode&#xff09; -》》》》》题目链接 实现思路&#xff1a;计算数字符串长度并用数组的方式计算出字符位置&#xff0c;用字符的ask码‘0’计算出字符本身。 class Solution { public:string addStrings(string num1, string num2) {string str;int…

easyexcel实现自定义的策略类, 最后追加错误提示列, 自适应列宽,自动合并重复单元格, 美化表头

easyexcel实现自定义的策略类, 最后追加错误提示列, 自适应列宽,自动合并重复单元格, 美化表头 原版表头和表体字体美化自动拼接错误提示列自适应宽度自动合并单元格使用Easyexcel使用poi导出 在后台管理开发的工作中,离不开的就是导出excel了. 如果是简单的导出, 直接easyexce…

brainpy 动力学编程基础

文章参考&#xff1a; 《神经计算建模实战——基于brainpy》 吴思 【brainpy学习笔记】基础知识2(动力学模型的编程基础)-CSDN博客 Brainpy手册 文章目录 积分器&#xff1a;定义ODE函数数值积分方法 更新函数和动力系统计算介绍什么是brainpy.DynamicalSystem&#xff1f;如…

高级图像处理工具

图像处理-高级 1、功能概览 随着社交媒体的普及和个人创作需求的增长&#xff0c;图像处理成为了日常生活中不可或缺的一部分。无论是专业的设计师还是爱好者&#xff0c;都需要一款强大的工具来帮助他们完成各种任务。今天&#xff0c;我们将介绍一款基于Python开发的高级图…

【Zookeeper集群搭建】安装zookeeper、zookeeper集群配置、zookeeper启动与关闭、zookeeper的shell命令操作

目录 一、安装Zookeeper 二、配置Zookeeper集群 三、Zookeeper服务的启动与关闭 四、Zookeeper的shell操作 前情提要&#xff1a;延续上篇【Hadoop和Hbase集群配置】继续配置Zookeeper&#xff0c;开启三台虚拟机Hadoop1、Hadoop2、Hadoop3&#xff0c;进入终端&#xff0c…

Transformer和BERT的区别

Transformer和BERT的区别比较表&#xff1a; 两者的位置编码&#xff1a; 为什么要对位置进行编码&#xff1f; Attention提取特征的时候&#xff0c;可以获取全局每个词对之间的关系&#xff0c;但是并没有显式保留时序信息&#xff0c;或者说位置信息。就算打乱序列中token…

Python爬虫如何处理验证码与登录

Python爬虫如何处理验证码与登录 Python 爬虫在抓取需要登录的网站数据时&#xff0c;通常会遇到两个主要问题&#xff1a;登录验证和验证码处理。这些机制是网站用来防止自动化程序过度抓取数据的主要手段。本文将详细讲解如何使用 Python 处理登录与验证码&#xff0c;以便进…

《深入浅出Apache Spark》系列②:Spark SQL原理精髓全解析

导读&#xff1a;SQL 诞生于 20 世纪 70 年代&#xff0c;至今已有半个世纪。SQL 语言具有语法简单&#xff0c;低学习门槛等特点&#xff0c;诞生之后迅速普及与流行开来。由于 SQL 具有易学易用的特点&#xff0c;使得开发人员容易掌握&#xff0c;企业若能在其计算机软件中支…

JS实现,防抖节流 + 闭包

防抖&#xff08;Debounce&#xff09; 防抖是指短时间内大量触发同一事件&#xff0c;只会在最后一次事件完成后延迟执行一次函数。 防抖的典型应用场景是输入框的搜索建议功能&#xff0c;用户输入时不需要每次输入都去查询&#xff0c;而是在用户停止输入一段时间后才进行…

安卓编程最方便的读写资料类SharedPreferences,多个APP共享

本文介绍Android平台进行数据存储的五大方式,分别如下: 1 使用SharedPreferences存储数据 2 文件存储数据 3 SQLite数据库存储数据 4 使用ContentProvider存储数据 5 网络存储数据 下面详细讲解这五种方式的特点 第一种&#xff1a; 使用SharedPreferences存储数据 …

数据分析:转录组差异fgsea富集分析

文章目录 介绍加载R包数据链接导入数据数据预处理DE testing: 2BP vs no-BP比较limma-voomLoad steroid dataIn No-BP patientsIn 2BP patientsCompare gene expression vs bacterial mass其他系统信息介绍 转录组差异fgsea富集分析是一种基于基因集的富集分析方法,它关注的是…

Day13杨辉三角

给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 class Solution {public List<List<Integer>> generate(int numRows) {List<List<Integer>> res new Arra…

Avalonia11如何优雅的跨组件通信

背景&#xff1a; 官网只介绍了推荐适用ReactiveUI&#xff0c;没有过多的案例介绍&#xff0c;对于初入桌面应用开发的小白极其不友好。 本文介绍在Avalonia应用中通过ReactiveUI中的MessageBus进行跨组件通信. 假设需求案例&#xff1a; MainWindowViewModel中发送消息&a…

【开发实战】彻底了解 ThreadLocal

👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO 专家博主 ⛪️ 个人社区:个人社区 💞 个人主页:个人主页 🙉 专栏地址: ✅ Java 中级 🙉八股文专题:剑指大厂,手撕 J…

基于开源 AI 智能名片、S2B2C 商城小程序的用户获取成本优化分析

摘要&#xff1a;本文围绕用户获取成本&#xff08;CAC&#xff09;这一关键指标展开深入剖析&#xff0c;详细阐述其计算方式&#xff0c;并紧密结合开源 AI 智能名片与 S2B2C 商城小程序的独特性质&#xff0c;从多个维度探讨如何通过挖掘新的获客渠道、巧妙运用私域流量池等…

KV260 - PYNQ 主目录 - U盘挂载

目录 1. 简介 2. 具体操作 2.1 查看 USB 设备 2.2 查看 U 盘设备节点 2.3 挂载 U 盘到指定目录 2.4 查看挂载状态 2.5 卸载 U 盘 3. 总结 1. 简介 在 KV260 使用 Jupyter Lab 可以非常方便开发各种应用。有时不方便在 PC 端连接 U 盘&#xff0c;那么可以把 U 盘连在 …

金媒婚恋相亲系统10.4择爱开源旗舰版支持微信小程和抖音小程序上架

最近大家应该注意到了&#xff0c;金媒婚恋相亲系统已经更新至最新的10.4版本了&#xff01;本人作为商业用户也已经更新至最新的旗舰版了&#xff0c;更新的内容是啥&#xff01;这个官方都有列出&#xff0c;一个方面就是更新了多端的登录逻辑和UI 和后台CRM及很多细节的优化…