Foundry 单元测试

安装 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 ethervm.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 ethervm.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 ethervm.startPrank(buyer); // msg.sender == buyerdeposit.buyerDeposit{value: 1 ether}();assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // 检查合约的余额是否增加vm.stopPrank();// 三天后卖家提款vm.startPrank(SELLER); // msg.sender == SELLERvm.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 ethervm.startPrank(buyer); // msg.sender == buyerdeposit.buyerDeposit{value: 1 ether}();assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // 检查合约的余额是否增加vm.stopPrank();// 三天前卖家提款vm.startPrank(SELLER); // msg.sender == SELLERvm.warp(1680616584 + 2 days);vm.expectRevert(); // 预期会回滚deposit.sellerWithdraw(address(this));}function testdepositTimeMatchesTimeofTransaction() public startAtPresentDay {// 此测试检查公共变量 depositTime 是否与交易时间匹配vm.startPrank(buyer); // msg.sender == buyerdeposit.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 == buyerdeposit.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 == SELLERvm.expectRevert();deposit.sellerWithdraw(buyer); }function testBuyerBuysAgain() public startAtPresentDay {// 此测试检查买家的条目是否被删除(这允许买家重新购买)vm.startPrank(buyer); // msg.sender == buyerdeposit.buyerDeposit{value: 1 ether}();vm.stopPrank();// 卖家提款vm.warp(1680616584 + 3 days + 1 seconds);vm.startPrank(SELLER); // msg.sender == SELLERdeposit.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.mzph.cn/bicheng/59631.shtml

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

相关文章

Anaconda超详细下载安装教程(附安装包)

文章目录 一、下载二、安装Anaconda1.解压下载的安装包2.开始安装3.测试配置是否成功4.其他问题1.查看Anaconda版本2.查看当前是否可以使用python 一、下载 Anaconda安装包下载&#xff1a;https://pan.quark.cn/s/ae29fb506730 &#xff08;直接下载&#xff0c;解压安装即可…

Go-性能优化、优化分析、调优实战pprof

使用官方自带benchmark进行基准性能测试 第一个是函数名-核数 第二个是执行次数 第三个是一次执行时间 第四个是一次执行的多大的内存 第五个是一次执行申请几次内存 slice用的时候在make&#xff08;&#xff09;初始化切片时提供容量信息 data:make([]int,0) data:make([]in…

docker安装低版本的jenkins-2.346.3,在线安装对应版本插件失败的解决方法

提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、网上最多的默认解决方法1、jenkins界面配置清华源2、替换default.json文件 二、解决低版本Jenkins在线安装插件问题1.手动下载插件并导入2.低版本jenkins在…

android 使用xml设置背景图片和圆角

使用xml设置背景图片和圆角 <?xml version"1.0" encoding"utf-8"?> <layer-list xmlns:android"http://schemas.android.com/apk/res/android"><item><shape><solid android:color"android:color/transparen…

SIwave:释放信号网络分析仪的强大功能

SIwave 是一种电源完整性和信号完整性工具。信号网络分析器求解器是 SIwave 中的工具之一。 Signal Net Analyzer 是 SIwave 信号完整性包的一部分。它可以快速计算走线的信号完整性。它计算阻抗、延迟、损耗和许多其他东西。它允许用户研究迹线损耗和任何缺陷对注入信号的影响…

stm32 踩坑笔记

串口问题&#xff1a; 问题&#xff1a;会改变接收缓冲的下一个字节 串口的初始化如下&#xff0c;位长度选择了9位。因为要奇偶校验&#xff0c;要选择9位。但是接收有用数据只用到1个字节。 问题原因&#xff1a; 所以串口接收时会把下一个数据更改

在 CSS 中,gap 是 布局容器(flex 或 grid)的属性。它用于设置容器内子元素之间的间距。

在 CSS 中&#xff0c;gap 是 布局容器&#xff08;flex 或 grid&#xff09;的属性。它用于设置容器内子元素之间的间距。以下是 gap 属性在不同布局中的应用&#xff1a; 1. 在 CSS Grid 布局中 gap 定义了网格行和列之间的间距。可以分别使用 row-gap 和 column-gap 设置行…

可控视频生成论文/数据/模型/代码总结

https://github.com/wangqiang9/Awesome-Controllable-Video-Diffusion Table of Contents Pose ControlAudio ControlUniversal ControlCamera ControlTrajectory Control

Linux权限解析:用户、组和权限的协同

​​​​​​​在Linux系统中&#xff0c;权限决定了谁能做什么。本文将指导你如何掌握这些权限&#xff0c;以确保你的系统既安全又高效&#xff01; 目录 1.shell命令及其运行原理 2.Linu权限的概念 (1) 用户 (2) 切换用户命令su (3) 指令提权命令sudo (4) 什么是权限…

神经网络基础--什么是神经网络?? 常用激活函数是什么???

前言 本专栏更新神经网络的一些基础知识&#xff1b;案例代码基于pytorch&#xff1b;欢迎收藏 关注&#xff0c; 本人将会持续更新。 神经网络 1、什么是神经网络 人工神经网络&#xff08; Artificial Neural Network&#xff0c; 简写为ANN&#xff09;也简称为神经网络…

猫头虎分享: AI设计利器 Recraft详解与基础使用教程

&#x1f981;猫头虎分享&#xff1a;AI设计利器 Recraft——全面解析与教程 大家好&#xff0c;我是猫头虎&#xff01;今天为大家带来一款非常炙手可热的 AI 设计工具 —— Recraft 的深度介绍与详细教程。这款工具自推出以来&#xff0c;就迅速获得了全球设计师的青睐。那么…

[MySQL]视图

视图是什么 视图&#xff08;View&#xff09;是一种虚拟存在的表。视图中的数据&#xff0c;来自定义视图的查询语句中&#xff0c;使用的表&#xff0c;并且是在使用视图时动态生成的。 简单讲&#xff0c;视图只保存了查询的SQL逻辑&#xff0c;不保存查询结果。所以我们在…

Python进阶之IO操作

文章目录 一、文件的读取二、文件内容的写入三、之操作文件夹四、StringIO与BytesIO 一、文件的读取 在python里面&#xff0c;可以使用open函数来打开文件&#xff0c;具体语法如下&#xff1a; open(filename, mode)filename&#xff1a;文件名&#xff0c;一般包括该文件所…

《安富莱嵌入式周报》第345期:开源蓝牙游戏手柄,USB3.0 HUB带电压电流测量,LCR电桥前端模拟,开源微型赛车,RF信号扫描仪,开源无线电收发器

周报汇总地址&#xff1a;嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 本周更新一期视频教程 第5期&#xff1a;RTX5/FreeRTOS全家桶源码工程综合实战模板集成CANopen组件&#xff08;2024-1…

「Mac畅玩鸿蒙与硬件20」鸿蒙UI组件篇10 - Canvas 组件自定义绘图

Canvas 组件在鸿蒙应用中用于绘制自定义图形&#xff0c;提供丰富的绘制功能和灵活的定制能力。通过 Canvas&#xff0c;可以创建矩形、圆形、路径、文本等基础图形&#xff0c;为鸿蒙应用增添个性化的视觉效果。本篇将介绍 Canvas 组件的基础操作&#xff0c;涵盖绘制矩形、圆…

用流量策略做多出口实验

一、拓扑&#xff1a; 二、配置过程&#xff1a; 1、配置 IP 地址&#xff0c;配置动态路由协议 OSPF 2、AR2 上&#xff0c;配置高级 ACL&#xff0c;允许 ospf 流量、1 到 6、2 到 8、deny 所有 3、写流分类&#xff0c;抓取流量特征 4、写流行为&#xff0c;配置流量动作 5、…

【Golang】sql.Null* 类型使用(处理空值和零值)

sql.NullString 和 sql.NullInt64 类型&#xff08;以及其他类似的 sql.Null* 类型&#xff09;在处理数据库操作时非常有用&#xff0c;尤其是在 Go 语言的 database/sql 包中。它们的主要用途包括&#xff1a; 表示 NULL 值&#xff1a; 在数据库中&#xff0c;NULL 表示“没…

HTML 鼠标滑动 页面的header背景从透明色变为黑色

要实现当鼠标滑动时&#xff0c;页面的header背景从透明色变为黑色&#xff0c;你可以使用JavaScript来监听滚动事件&#xff0c;并根据页面的滚动位置来改变header的背景颜色。 <!DOCTYPE html> <html lang"en"> <head> <meta charset"U…

【前端】如何在 JSX 中使用条件语句和循环

在 JSX 中使用条件语句和循环是常见的需求&#xff0c;尤其是在构建动态用户界面时。以下是如何在 JSX 中使用条件语句和循环的详细说明。 条件语句 1. 三元运算符 三元运算符是最简单的条件语句形式&#xff0c;适用于简单的条件判断。 const isLoggedIn true;const ele…

Scala 的访问权限

1.1 Scala的访问权限概述 Scala中的属性成员、方法和构造器这三种变量可以通过访问控制符控制访问权限。不同的访问控制符可以决定是否可以被外部类访问。由于局部方法的作用域本身有局限&#xff0c;所以不需要使用访问控制符修饰局部方法。属性成员、方法和构造器的访问控制权…